From e2675584e2439efb23281b3d71b386ffdb6e3d9f Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Thu, 6 Feb 2025 15:11:51 +0100 Subject: [PATCH] Add dump-effective-configuration subcommand closes #667 Implement the subcommand `dump-effective-configuration`, which dumps the configuration that would be used if `swift-format` was executed from the current working directory (cwd), incorporating configuration files found in the cwd or its parents, or input from the `--configuration` option. This helps when composing a configuration or with configuration debugging/verification activities. --- README.md | 16 ++++-- .../SwiftFormat/API/Configuration+Dump.swift | 39 ++++++++++++++ .../SwiftFormat/API/SwiftFormatError.swift | 5 ++ Sources/SwiftFormat/CMakeLists.txt | 3 +- Sources/swift-format/CMakeLists.txt | 6 ++- .../DumpEffectiveConfigurationFrontend.swift | 25 +++++++++ .../Frontend/FormatFrontend.swift | 6 +-- Sources/swift-format/Frontend/Frontend.swift | 12 +++-- .../Subcommands/ConfigurationOptions.swift | 28 ++++++++++ .../Subcommands/DumpConfiguration.swift | 21 +------- .../DumpEffectiveConfiguration.swift | 54 +++++++++++++++++++ Sources/swift-format/Subcommands/Format.swift | 11 +++- Sources/swift-format/Subcommands/Lint.swift | 7 ++- .../Subcommands/LintFormatOptions.swift | 14 +---- Sources/swift-format/SwiftFormatCommand.swift | 3 +- .../swift-format/Utilities/FormatError.swift | 23 -------- 16 files changed, 198 insertions(+), 75 deletions(-) create mode 100644 Sources/SwiftFormat/API/Configuration+Dump.swift create mode 100644 Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift create mode 100644 Sources/swift-format/Subcommands/ConfigurationOptions.swift create mode 100644 Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift delete mode 100644 Sources/swift-format/Utilities/FormatError.swift diff --git a/README.md b/README.md index db2e090a9..94aace284 100644 --- a/README.md +++ b/README.md @@ -210,13 +210,19 @@ settings in the default configuration can be viewed by running `swift-format dump-configuration`, which will dump it to standard output. -If the `--configuration ` option is passed to `swift-format`, then that -configuration will be used unconditionally and the file system will not be -searched. +If the `--configuration ` option is passed to `swift-format`, +then that configuration will be used unconditionally and the file system will +not be searched. See [Documentation/Configuration.md](Documentation/Configuration.md) for a -description of the configuration file format and the settings that are -available. +description of the configuration format and the settings that are available. + +#### Viewing the Effective Configuration + +The `dump-effective-configuration` subcommand dumps the configuration that +would be used if `swift-format` was executed from the current working directory, +and accounts for `.swift-format` files or `--configuration` options as outlined +above. ### Miscellaneous diff --git a/Sources/SwiftFormat/API/Configuration+Dump.swift b/Sources/SwiftFormat/API/Configuration+Dump.swift new file mode 100644 index 000000000..a9a68fb7e --- /dev/null +++ b/Sources/SwiftFormat/API/Configuration+Dump.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension Configuration { + /// Return the configuration as a JSON string. + public func asJsonString() throws -> String { + let data: Data + + do { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + if #available(macOS 10.13, *) { + encoder.outputFormatting.insert(.sortedKeys) + } + + data = try encoder.encode(self) + } catch { + throw SwiftFormatError.configurationDumpFailed("\(error)") + } + + guard let jsonString = String(data: data, encoding: .utf8) else { + // This should never happen, but let's make sure we fail more gracefully than crashing, just in case. + throw SwiftFormatError.configurationDumpFailed("The JSON was not valid UTF-8") + } + + return jsonString + } +} diff --git a/Sources/SwiftFormat/API/SwiftFormatError.swift b/Sources/SwiftFormat/API/SwiftFormatError.swift index 6e4183162..cb35a82d0 100644 --- a/Sources/SwiftFormat/API/SwiftFormatError.swift +++ b/Sources/SwiftFormat/API/SwiftFormatError.swift @@ -28,6 +28,9 @@ public enum SwiftFormatError: LocalizedError { /// The requested experimental feature name was not recognized by the parser. case unrecognizedExperimentalFeature(String) + /// An error happened while dumping the tool's configuration. + case configurationDumpFailed(String) + public var errorDescription: String? { switch self { case .fileNotReadable: @@ -38,6 +41,8 @@ public enum SwiftFormatError: LocalizedError { return "file contains invalid Swift syntax" case .unrecognizedExperimentalFeature(let name): return "experimental feature '\(name)' was not recognized by the Swift parser" + case .configurationDumpFailed(let message): + return "dumping configuration failed: \(message)" } } } diff --git a/Sources/SwiftFormat/CMakeLists.txt b/Sources/SwiftFormat/CMakeLists.txt index 46937f713..9306c49f3 100644 --- a/Sources/SwiftFormat/CMakeLists.txt +++ b/Sources/SwiftFormat/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the swift-format open source project -Copyright (c) 2024 Apple Inc. and the swift-format project authors +Copyright (c) 2024 - 2025 Apple Inc. and the swift-format project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information @@ -9,6 +9,7 @@ See https://swift.org/LICENSE.txt for license information add_library(SwiftFormat API/Configuration+Default.swift + API/Configuration+Dump.swift API/Configuration.swift API/DebugOptions.swift API/Finding.swift diff --git a/Sources/swift-format/CMakeLists.txt b/Sources/swift-format/CMakeLists.txt index 9ae9603e1..b563a4463 100644 --- a/Sources/swift-format/CMakeLists.txt +++ b/Sources/swift-format/CMakeLists.txt @@ -1,7 +1,7 @@ #[[ This source file is part of the swift-format open source project -Copyright (c) 2024 Apple Inc. and the swift-format project authors +Copyright (c) 2024 - 2025 Apple Inc. and the swift-format project authors Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information @@ -12,10 +12,13 @@ add_executable(swift-format SwiftFormatCommand.swift VersionOptions.swift Frontend/ConfigurationLoader.swift + Frontend/DumpEffectiveConfigurationFrontend.swift Frontend/FormatFrontend.swift Frontend/Frontend.swift Frontend/LintFrontend.swift + Subcommands/ConfigurationOptions.swift Subcommands/DumpConfiguration.swift + Subcommands/DumpEffectiveConfiguration.swift Subcommands/Format.swift Subcommands/Lint.swift Subcommands/LintFormatOptions.swift @@ -23,7 +26,6 @@ add_executable(swift-format Utilities/Diagnostic.swift Utilities/DiagnosticsEngine.swift Utilities/FileHandleTextOutputStream.swift - Utilities/FormatError.swift Utilities/StderrDiagnosticPrinter.swift Utilities/TTY.swift) target_link_libraries(swift-format PRIVATE diff --git a/Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift b/Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift new file mode 100644 index 000000000..1608c9aa8 --- /dev/null +++ b/Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftFormat + +/// The frontend for dumping the effective configuration. +class DumpEffectiveConfigurationFrontend: Frontend { + private(set) var dumpResult: Result = .failure( + SwiftFormatError.configurationDumpFailed("Configuration not resolved yet") + ) + + override func processFile(_ fileToProcess: FileToProcess) { + dumpResult = Result.init(catching: fileToProcess.configuration.asJsonString) + } +} diff --git a/Sources/swift-format/Frontend/FormatFrontend.swift b/Sources/swift-format/Frontend/FormatFrontend.swift index 23d127719..a205b6405 100644 --- a/Sources/swift-format/Frontend/FormatFrontend.swift +++ b/Sources/swift-format/Frontend/FormatFrontend.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -20,9 +20,9 @@ class FormatFrontend: Frontend { /// Whether or not to format the Swift file in-place. private let inPlace: Bool - init(lintFormatOptions: LintFormatOptions, inPlace: Bool) { + init(configurationOptions: ConfigurationOptions, lintFormatOptions: LintFormatOptions, inPlace: Bool) { self.inPlace = inPlace - super.init(lintFormatOptions: lintFormatOptions) + super.init(configurationOptions: configurationOptions, lintFormatOptions: lintFormatOptions) } override func processFile(_ fileToProcess: FileToProcess) { diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index b0e262a94..d7bb5cbe5 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -67,6 +67,9 @@ class Frontend { /// The diagnostic engine to which warnings and errors will be emitted. final let diagnosticsEngine: DiagnosticsEngine + /// Options that control the tool's configuration. + final let configurationOptions: ConfigurationOptions + /// Options that apply during formatting or linting. final let lintFormatOptions: LintFormatOptions @@ -85,7 +88,8 @@ class Frontend { /// Creates a new frontend with the given options. /// /// - Parameter lintFormatOptions: Options that apply during formatting or linting. - init(lintFormatOptions: LintFormatOptions) { + init(configurationOptions: ConfigurationOptions, lintFormatOptions: LintFormatOptions) { + self.configurationOptions = configurationOptions self.lintFormatOptions = lintFormatOptions self.diagnosticPrinter = StderrDiagnosticPrinter( @@ -139,7 +143,7 @@ class Frontend { guard let configuration = configuration( - fromPathOrString: lintFormatOptions.configuration, + fromPathOrString: configurationOptions.configuration, orInferredFromSwiftFileAt: assumedUrl ) else { @@ -190,7 +194,7 @@ class Frontend { guard let configuration = configuration( - fromPathOrString: lintFormatOptions.configuration, + fromPathOrString: configurationOptions.configuration, orInferredFromSwiftFileAt: url ) else { diff --git a/Sources/swift-format/Subcommands/ConfigurationOptions.swift b/Sources/swift-format/Subcommands/ConfigurationOptions.swift new file mode 100644 index 000000000..9c018a111 --- /dev/null +++ b/Sources/swift-format/Subcommands/ConfigurationOptions.swift @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser + +/// Common arguments used by the `lint`, `format` and `dump-effective-configuration` subcommands. +struct ConfigurationOptions: ParsableArguments { + /// The path to the JSON configuration file that should be loaded. + /// + /// If not specified, the default configuration will be used. + @Option( + name: .customLong("configuration"), + help: """ + The path to a JSON file containing the configuration of the linter/formatter or a JSON string containing the \ + configuration directly. + """ + ) + var configuration: String? +} diff --git a/Sources/swift-format/Subcommands/DumpConfiguration.swift b/Sources/swift-format/Subcommands/DumpConfiguration.swift index ff41c8554..66964e9c7 100644 --- a/Sources/swift-format/Subcommands/DumpConfiguration.swift +++ b/Sources/swift-format/Subcommands/DumpConfiguration.swift @@ -22,26 +22,9 @@ extension SwiftFormatCommand { ) func run() throws { - let configuration = Configuration() - do { - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted] - if #available(macOS 10.13, *) { - encoder.outputFormatting.insert(.sortedKeys) - } + let configuration = try Configuration().asJsonString() - let data = try encoder.encode(configuration) - guard let jsonString = String(data: data, encoding: .utf8) else { - // This should never happen, but let's make sure we fail more gracefully than crashing, just - // in case. - throw FormatError( - message: "Could not dump the default configuration: the JSON was not valid UTF-8" - ) - } - print(jsonString) - } catch { - throw FormatError(message: "Could not dump the default configuration: \(error)") - } + print(configuration) } } } diff --git a/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift b/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift new file mode 100644 index 000000000..277823ca0 --- /dev/null +++ b/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import SwiftFormat + +extension SwiftFormatCommand { + /// Dumps the tool's effective configuration in JSON format to standard output. + struct DumpEffectiveConfiguration: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Dump the effective configuration in JSON format to standard output", + discussion: """ + Dumps the configuration that would be used if swift-format was executed from the current working \ + directory (cwd), incorporating configuration files found in the cwd or its parents, or input from the \ + --configuration option. + """ + ) + + @OptionGroup() + var configurationOptions: ConfigurationOptions + + func run() throws { + // Pretend to use stdin, so that the configuration loading machinery in the Frontend base class can be used in the + // next step. This produces the same results as if "format" or "lint" subcommands were called. + let lintFormatOptions = try LintFormatOptions.parse(["-"]) + + let frontend = DumpEffectiveConfigurationFrontend( + configurationOptions: configurationOptions, + lintFormatOptions: lintFormatOptions + ) + frontend.run() + if frontend.diagnosticsEngine.hasErrors { + throw ExitCode.failure + } + + switch frontend.dumpResult { + case .success(let configuration): + print(configuration) + case .failure(let error): + throw error + } + } + } +} diff --git a/Sources/swift-format/Subcommands/Format.swift b/Sources/swift-format/Subcommands/Format.swift index 42c2da165..59da36ffb 100644 --- a/Sources/swift-format/Subcommands/Format.swift +++ b/Sources/swift-format/Subcommands/Format.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -29,6 +29,9 @@ extension SwiftFormatCommand { ) var inPlace: Bool = false + @OptionGroup() + var configurationOptions: ConfigurationOptions + @OptionGroup() var formatOptions: LintFormatOptions @@ -43,7 +46,11 @@ extension SwiftFormatCommand { func run() throws { try performanceMeasurementOptions.printingInstructionCountIfRequested() { - let frontend = FormatFrontend(lintFormatOptions: formatOptions, inPlace: inPlace) + let frontend = FormatFrontend( + configurationOptions: configurationOptions, + lintFormatOptions: formatOptions, + inPlace: inPlace + ) frontend.run() if frontend.diagnosticsEngine.hasErrors { throw ExitCode.failure } } diff --git a/Sources/swift-format/Subcommands/Lint.swift b/Sources/swift-format/Subcommands/Lint.swift index 3002c5912..cee4cee41 100644 --- a/Sources/swift-format/Subcommands/Lint.swift +++ b/Sources/swift-format/Subcommands/Lint.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -20,6 +20,9 @@ extension SwiftFormatCommand { discussion: "When no files are specified, it expects the source from standard input." ) + @OptionGroup() + var configurationOptions: ConfigurationOptions + @OptionGroup() var lintOptions: LintFormatOptions @@ -34,7 +37,7 @@ extension SwiftFormatCommand { func run() throws { try performanceMeasurementOptions.printingInstructionCountIfRequested { - let frontend = LintFrontend(lintFormatOptions: lintOptions) + let frontend = LintFrontend(configurationOptions: configurationOptions, lintFormatOptions: lintOptions) frontend.run() if frontend.diagnosticsEngine.hasErrors || strict && frontend.diagnosticsEngine.hasWarnings { diff --git a/Sources/swift-format/Subcommands/LintFormatOptions.swift b/Sources/swift-format/Subcommands/LintFormatOptions.swift index 737de42fc..ca1232912 100644 --- a/Sources/swift-format/Subcommands/LintFormatOptions.swift +++ b/Sources/swift-format/Subcommands/LintFormatOptions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -15,18 +15,6 @@ import Foundation /// Common arguments used by the `lint` and `format` subcommands. struct LintFormatOptions: ParsableArguments { - /// The path to the JSON configuration file that should be loaded. - /// - /// If not specified, the default configuration will be used. - @Option( - name: .customLong("configuration"), - help: """ - The path to a JSON file containing the configuration of the linter/formatter or a JSON \ - string containing the configuration directly. - """ - ) - var configuration: String? - /// A list of comma-separated "start:end" pairs specifying UTF-8 offsets of the ranges to format. /// /// If not specified, the whole file will be formatted. diff --git a/Sources/swift-format/SwiftFormatCommand.swift b/Sources/swift-format/SwiftFormatCommand.swift index 5b814a159..cdcb77fa0 100644 --- a/Sources/swift-format/SwiftFormatCommand.swift +++ b/Sources/swift-format/SwiftFormatCommand.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift.org open source project // -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -21,6 +21,7 @@ struct SwiftFormatCommand: ParsableCommand { abstract: "Format or lint Swift source code", subcommands: [ DumpConfiguration.self, + DumpEffectiveConfiguration.self, Format.self, Lint.self, ], diff --git a/Sources/swift-format/Utilities/FormatError.swift b/Sources/swift-format/Utilities/FormatError.swift deleted file mode 100644 index b922038ee..000000000 --- a/Sources/swift-format/Utilities/FormatError.swift +++ /dev/null @@ -1,23 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -import Foundation - -struct FormatError: LocalizedError { - var message: String - var errorDescription: String? { message } - - static var exitWithDiagnosticErrors: FormatError { - // The diagnostics engine has already printed errors to stderr. - FormatError(message: "") - } -}