From 6e488242a5d5dfd0b6402d4caf4fd8d64173ad53 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Thu, 6 Feb 2025 15:11:51 +0100 Subject: [PATCH 1/4] 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: "") - } -} From 8172c06f9391213b932ada7f428a6496cc8c92a7 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Thu, 6 Feb 2025 19:59:41 +0100 Subject: [PATCH 2/4] Merge into single subcommand --- README.md | 8 +-- Sources/swift-format/CMakeLists.txt | 3 +- ....swift => DumpConfigurationFrontend.swift} | 10 ++-- .../Subcommands/ConfigurationOptions.swift | 2 +- .../Subcommands/DumpConfiguration.swift | 50 +++++++++++++++-- .../DumpEffectiveConfiguration.swift | 54 ------------------- Sources/swift-format/SwiftFormatCommand.swift | 3 +- 7 files changed, 57 insertions(+), 73 deletions(-) rename Sources/swift-format/Frontend/{DumpEffectiveConfigurationFrontend.swift => DumpConfigurationFrontend.swift} (71%) delete mode 100644 Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift diff --git a/README.md b/README.md index 94aace284..77342ff5c 100644 --- a/README.md +++ b/README.md @@ -219,10 +219,10 @@ 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. +The `dump-configuration` subcommand accepts a `--effective` flag. If set, it +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/swift-format/CMakeLists.txt b/Sources/swift-format/CMakeLists.txt index b563a4463..ef77a26b0 100644 --- a/Sources/swift-format/CMakeLists.txt +++ b/Sources/swift-format/CMakeLists.txt @@ -12,13 +12,12 @@ add_executable(swift-format SwiftFormatCommand.swift VersionOptions.swift Frontend/ConfigurationLoader.swift - Frontend/DumpEffectiveConfigurationFrontend.swift + Frontend/DumpConfigurationFrontend.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 diff --git a/Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift b/Sources/swift-format/Frontend/DumpConfigurationFrontend.swift similarity index 71% rename from Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift rename to Sources/swift-format/Frontend/DumpConfigurationFrontend.swift index 1608c9aa8..5bae2451f 100644 --- a/Sources/swift-format/Frontend/DumpEffectiveConfigurationFrontend.swift +++ b/Sources/swift-format/Frontend/DumpConfigurationFrontend.swift @@ -13,13 +13,13 @@ 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") +/// The frontend for dumping the configuration. +class DumpConfigurationFrontend: Frontend { + private(set) var dumpedConfiguration: Result = .failure( + SwiftFormatError.configurationDumpFailed("Configuration not dumped yet") ) override func processFile(_ fileToProcess: FileToProcess) { - dumpResult = Result.init(catching: fileToProcess.configuration.asJsonString) + dumpedConfiguration = Result.init(catching: fileToProcess.configuration.asJsonString) } } diff --git a/Sources/swift-format/Subcommands/ConfigurationOptions.swift b/Sources/swift-format/Subcommands/ConfigurationOptions.swift index 9c018a111..1025715e9 100644 --- a/Sources/swift-format/Subcommands/ConfigurationOptions.swift +++ b/Sources/swift-format/Subcommands/ConfigurationOptions.swift @@ -12,7 +12,7 @@ import ArgumentParser -/// Common arguments used by the `lint`, `format` and `dump-effective-configuration` subcommands. +/// Common arguments used by the `lint`, `format` and `dump-configuration` subcommands. struct ConfigurationOptions: ParsableArguments { /// The path to the JSON configuration file that should be loaded. /// diff --git a/Sources/swift-format/Subcommands/DumpConfiguration.swift b/Sources/swift-format/Subcommands/DumpConfiguration.swift index 66964e9c7..753d4879e 100644 --- a/Sources/swift-format/Subcommands/DumpConfiguration.swift +++ b/Sources/swift-format/Subcommands/DumpConfiguration.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 @@ -15,16 +15,56 @@ import Foundation import SwiftFormat extension SwiftFormatCommand { - /// Dumps the tool's default configuration in JSON format to standard output. + /// Dumps the tool's configuration in JSON format to standard output. struct DumpConfiguration: ParsableCommand { static var configuration = CommandConfiguration( - abstract: "Dump the default configuration in JSON format to standard output" + abstract: "Dump the configuration in JSON format to standard output", + discussion: """ + Without any options, dumps the default configuration. When '--effective' is set, 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. + """ ) + /// Whether or not to dump the effective configuration. + @Flag(name: .shortAndLong, help: "Dump the effective instead of the default configuration.") + var effective: Bool = false + + @OptionGroup() + var configurationOptions: ConfigurationOptions + + func validate() throws { + if configurationOptions.configuration != nil && !effective { + throw ValidationError("'--configuration' is only valid in combination with '--effective'") + } + } + func run() throws { - let configuration = try Configuration().asJsonString() + if !effective { + let configuration = try Configuration().asJsonString() + print(configuration) + return + } + + // 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 = DumpConfigurationFrontend( + configurationOptions: configurationOptions, + lintFormatOptions: lintFormatOptions + ) + frontend.run() + if frontend.diagnosticsEngine.hasErrors { + throw ExitCode.failure + } - print(configuration) + switch frontend.dumpedConfiguration { + case .success(let configuration): + print(configuration) + case .failure(let error): + throw error + } } } } diff --git a/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift b/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift deleted file mode 100644 index 277823ca0..000000000 --- a/Sources/swift-format/Subcommands/DumpEffectiveConfiguration.swift +++ /dev/null @@ -1,54 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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/SwiftFormatCommand.swift b/Sources/swift-format/SwiftFormatCommand.swift index cdcb77fa0..5b814a159 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 - 2025 Apple Inc. and the Swift project authors +// 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 @@ -21,7 +21,6 @@ struct SwiftFormatCommand: ParsableCommand { abstract: "Format or lint Swift source code", subcommands: [ DumpConfiguration.self, - DumpEffectiveConfiguration.self, Format.self, Lint.self, ], From 1e8c05ecb62ea0ddf198d7ff344dd908dbd0480d Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Tue, 11 Feb 2025 11:28:54 +0100 Subject: [PATCH 3/4] Do not use dummy frontend for effective config printing --- .../SwiftFormat/API/Configuration+Dump.swift | 6 +- Sources/swift-format/CMakeLists.txt | 1 - .../Frontend/DumpConfigurationFrontend.swift | 25 -- Sources/swift-format/Frontend/Frontend.swift | 226 ++++++++++-------- .../Subcommands/DumpConfiguration.swift | 44 ++-- 5 files changed, 147 insertions(+), 155 deletions(-) delete mode 100644 Sources/swift-format/Frontend/DumpConfigurationFrontend.swift diff --git a/Sources/SwiftFormat/API/Configuration+Dump.swift b/Sources/SwiftFormat/API/Configuration+Dump.swift index a9a68fb7e..cd063b664 100644 --- a/Sources/SwiftFormat/API/Configuration+Dump.swift +++ b/Sources/SwiftFormat/API/Configuration+Dump.swift @@ -19,11 +19,7 @@ extension Configuration { do { let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted] - if #available(macOS 10.13, *) { - encoder.outputFormatting.insert(.sortedKeys) - } - + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] data = try encoder.encode(self) } catch { throw SwiftFormatError.configurationDumpFailed("\(error)") diff --git a/Sources/swift-format/CMakeLists.txt b/Sources/swift-format/CMakeLists.txt index ef77a26b0..09590be42 100644 --- a/Sources/swift-format/CMakeLists.txt +++ b/Sources/swift-format/CMakeLists.txt @@ -12,7 +12,6 @@ add_executable(swift-format SwiftFormatCommand.swift VersionOptions.swift Frontend/ConfigurationLoader.swift - Frontend/DumpConfigurationFrontend.swift Frontend/FormatFrontend.swift Frontend/Frontend.swift Frontend/LintFrontend.swift diff --git a/Sources/swift-format/Frontend/DumpConfigurationFrontend.swift b/Sources/swift-format/Frontend/DumpConfigurationFrontend.swift deleted file mode 100644 index 5bae2451f..000000000 --- a/Sources/swift-format/Frontend/DumpConfigurationFrontend.swift +++ /dev/null @@ -1,25 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 configuration. -class DumpConfigurationFrontend: Frontend { - private(set) var dumpedConfiguration: Result = .failure( - SwiftFormatError.configurationDumpFailed("Configuration not dumped yet") - ) - - override func processFile(_ fileToProcess: FileToProcess) { - dumpedConfiguration = Result.init(catching: fileToProcess.configuration.asJsonString) - } -} diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index d7bb5cbe5..6e24b7965 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -16,6 +16,118 @@ import SwiftParser import SwiftSyntax class Frontend { + /// Provides formatter configurations for given `.swift` source files, configuration files or configuration strings. + struct ConfigurationProvider { + /// Loads formatter configuration files and chaches them in memory. + private var configurationLoader: ConfigurationLoader = ConfigurationLoader() + + /// The diagnostic engine to which warnings and errors will be emitted. + private let diagnosticsEngine: DiagnosticsEngine + + /// Creates a new instance with the given options. + /// + /// - Parameter diagnosticsEngine: The diagnostic engine to which warnings and errors will be emitted. + init(diagnosticsEngine: DiagnosticsEngine) { + self.diagnosticsEngine = diagnosticsEngine + } + + /// Checks if all the rules in the given configuration are supported by the registry. + /// + /// If there are any rules that are not supported, they are emitted as a warning. + private func checkForUnrecognizedRules(in configuration: Configuration) { + // If any rules in the decoded configuration are not supported by the registry, + // emit them into the diagnosticsEngine as warnings. + // That way they will be printed out, but we'll continue execution on the valid rules. + let invalidRules = configuration.rules.filter { !RuleRegistry.rules.keys.contains($0.key) } + for rule in invalidRules { + diagnosticsEngine.emitWarning("Configuration contains an unrecognized rule: \(rule.key)", location: nil) + } + } + + /// Returns the configuration that applies to the given `.swift` source file, when an explicit + /// configuration path is also perhaps provided. + /// + /// This method also checks for unrecognized rules within the configuration. + /// + /// - Parameters: + /// - pathOrString: A string containing either the path to a configuration file that will be + /// loaded, JSON configuration data directly, or `nil` to try to infer it from + /// `swiftFileURL`. + /// - swiftFileURL: The path to a `.swift` file, which will be used to infer the path to the + /// configuration file if `configurationFilePath` is nil. + /// + /// - Returns: If successful, the returned configuration is the one loaded from `pathOrString` if + /// it was provided, or by searching in paths inferred by `swiftFileURL` if one exists, or the + /// default configuration otherwise. If an error occurred when reading the configuration, a + /// diagnostic is emitted and `nil` is returned. If neither `pathOrString` nor `swiftFileURL` + /// were provided, a default `Configuration()` will be returned. + mutating func provide( + forConfigPathOrString pathOrString: String?, + orForSwiftFileAt swiftFileURL: URL? + ) -> Configuration? { + if let pathOrString = pathOrString { + // If an explicit configuration file path was given, try to load it and fail if it cannot be + // loaded. (Do not try to fall back to a path inferred from the source file path.) + let configurationFileURL = URL(fileURLWithPath: pathOrString) + do { + let configuration = try configurationLoader.configuration(at: configurationFileURL) + self.checkForUnrecognizedRules(in: configuration) + return configuration + } catch { + // If we failed to load this from the path, try interpreting the string as configuration + // data itself because the user might have written something like `--configuration '{...}'`, + let data = pathOrString.data(using: .utf8)! + if let configuration = try? Configuration(data: data) { + return configuration + } + + // Fail if the configuration flag was neither a valid file path nor valid configuration + // data. + diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)") + return nil + } + } + + // If no explicit configuration file path was given but a `.swift` source file path was given, + // then try to load the configuration by inferring it based on the source file path. + if let swiftFileURL = swiftFileURL { + do { + if let configuration = try configurationLoader.configuration(forPath: swiftFileURL) { + self.checkForUnrecognizedRules(in: configuration) + return configuration + } + // Fall through to the default return at the end of the function. + } catch { + diagnosticsEngine.emitError( + "Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)" + ) + return nil + } + } else { + // If reading from stdin and no explicit configuration file was given, + // walk up the file tree from the cwd to find a config. + + let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + // Definitely a Swift file. Definitely not a directory. Shhhhhh. + do { + if let configuration = try configurationLoader.configuration(forPath: cwd) { + self.checkForUnrecognizedRules(in: configuration) + return configuration + } + } catch { + diagnosticsEngine.emitError( + "Unable to read configuration for \(cwd): \(error.localizedDescription)" + ) + return nil + } + } + + // An explicit configuration has not been given, and one cannot be found. + // Return the default configuration. + return Configuration() + } + } + /// Represents a file to be processed by the frontend and any file-specific options associated /// with it. final class FileToProcess { @@ -73,8 +185,8 @@ class Frontend { /// Options that apply during formatting or linting. final let lintFormatOptions: LintFormatOptions - /// Loads formatter configuration files. - final var configurationLoader = ConfigurationLoader() + /// The provider for formatter configurations. + final var configurationProvider: ConfigurationProvider /// Advanced options that are useful for developing/debugging but otherwise not meant for general /// use. @@ -95,8 +207,8 @@ class Frontend { self.diagnosticPrinter = StderrDiagnosticPrinter( colorMode: lintFormatOptions.colorDiagnostics.map { $0 ? .on : .off } ?? .auto ) - self.diagnosticsEngine = - DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) + self.diagnosticsEngine = DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) + self.configurationProvider = ConfigurationProvider(diagnosticsEngine: self.diagnosticsEngine) } /// Runs the linter or formatter over the inputs. @@ -142,9 +254,9 @@ class Frontend { let assumedUrl = lintFormatOptions.assumeFilename.map(URL.init(fileURLWithPath:)) guard - let configuration = configuration( - fromPathOrString: configurationOptions.configuration, - orInferredFromSwiftFileAt: assumedUrl + let configuration = configurationProvider.provide( + forConfigPathOrString: configurationOptions.configuration, + orForSwiftFileAt: assumedUrl ) else { // Already diagnosed in the called method. @@ -193,9 +305,9 @@ class Frontend { } guard - let configuration = configuration( - fromPathOrString: configurationOptions.configuration, - orInferredFromSwiftFileAt: url + let configuration = configurationProvider.provide( + forConfigPathOrString: configurationOptions.configuration, + orForSwiftFileAt: url ) else { // Already diagnosed in the called method. @@ -210,98 +322,4 @@ class Frontend { ) } - /// Returns the configuration that applies to the given `.swift` source file, when an explicit - /// configuration path is also perhaps provided. - /// - /// This method also checks for unrecognized rules within the configuration. - /// - /// - Parameters: - /// - pathOrString: A string containing either the path to a configuration file that will be - /// loaded, JSON configuration data directly, or `nil` to try to infer it from - /// `swiftFilePath`. - /// - swiftFilePath: The path to a `.swift` file, which will be used to infer the path to the - /// configuration file if `configurationFilePath` is nil. - /// - /// - Returns: If successful, the returned configuration is the one loaded from `pathOrString` if - /// it was provided, or by searching in paths inferred by `swiftFilePath` if one exists, or the - /// default configuration otherwise. If an error occurred when reading the configuration, a - /// diagnostic is emitted and `nil` is returned. If neither `pathOrString` nor `swiftFilePath` - /// were provided, a default `Configuration()` will be returned. - private func configuration( - fromPathOrString pathOrString: String?, - orInferredFromSwiftFileAt swiftFileURL: URL? - ) -> Configuration? { - if let pathOrString = pathOrString { - // If an explicit configuration file path was given, try to load it and fail if it cannot be - // loaded. (Do not try to fall back to a path inferred from the source file path.) - let configurationFileURL = URL(fileURLWithPath: pathOrString) - do { - let configuration = try configurationLoader.configuration(at: configurationFileURL) - self.checkForUnrecognizedRules(in: configuration) - return configuration - } catch { - // If we failed to load this from the path, try interpreting the string as configuration - // data itself because the user might have written something like `--configuration '{...}'`, - let data = pathOrString.data(using: .utf8)! - if let configuration = try? Configuration(data: data) { - return configuration - } - - // Fail if the configuration flag was neither a valid file path nor valid configuration - // data. - diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)") - return nil - } - } - - // If no explicit configuration file path was given but a `.swift` source file path was given, - // then try to load the configuration by inferring it based on the source file path. - if let swiftFileURL = swiftFileURL { - do { - if let configuration = try configurationLoader.configuration(forPath: swiftFileURL) { - self.checkForUnrecognizedRules(in: configuration) - return configuration - } - // Fall through to the default return at the end of the function. - } catch { - diagnosticsEngine.emitError( - "Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)" - ) - return nil - } - } else { - // If reading from stdin and no explicit configuration file was given, - // walk up the file tree from the cwd to find a config. - - let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) - // Definitely a Swift file. Definitely not a directory. Shhhhhh. - do { - if let configuration = try configurationLoader.configuration(forPath: cwd) { - self.checkForUnrecognizedRules(in: configuration) - return configuration - } - } catch { - diagnosticsEngine.emitError( - "Unable to read configuration for \(cwd): \(error.localizedDescription)" - ) - return nil - } - } - - // An explicit configuration has not been given, and one cannot be found. - // Return the default configuration. - return Configuration() - } - - /// Checks if all the rules in the given configuration are supported by the registry. - /// If there are any rules that are not supported, they are emitted as a warning. - private func checkForUnrecognizedRules(in configuration: Configuration) { - // If any rules in the decoded configuration are not supported by the registry, - // emit them into the diagnosticsEngine as warnings. - // That way they will be printed out, but we'll continue execution on the valid rules. - let invalidRules = configuration.rules.filter { !RuleRegistry.rules.keys.contains($0.key) } - for rule in invalidRules { - diagnosticsEngine.emitWarning("Configuration contains an unrecognized rule: \(rule.key)", location: nil) - } - } } diff --git a/Sources/swift-format/Subcommands/DumpConfiguration.swift b/Sources/swift-format/Subcommands/DumpConfiguration.swift index 753d4879e..0014151a4 100644 --- a/Sources/swift-format/Subcommands/DumpConfiguration.swift +++ b/Sources/swift-format/Subcommands/DumpConfiguration.swift @@ -40,30 +40,34 @@ extension SwiftFormatCommand { } func run() throws { - if !effective { - let configuration = try Configuration().asJsonString() - print(configuration) - return - } + let diagnosticPrinter = StderrDiagnosticPrinter(colorMode: .auto) + let diagnosticsEngine = DiagnosticsEngine(diagnosticsHandlers: [diagnosticPrinter.printDiagnostic]) - // 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 configuration: Configuration + if effective { + var configurationProvider = Frontend.ConfigurationProvider(diagnosticsEngine: diagnosticsEngine) - let frontend = DumpConfigurationFrontend( - configurationOptions: configurationOptions, - lintFormatOptions: lintFormatOptions - ) - frontend.run() - if frontend.diagnosticsEngine.hasErrors { - throw ExitCode.failure + guard + let effectiveConfiguration = configurationProvider.provide( + forConfigPathOrString: configurationOptions.configuration, + orForSwiftFileAt: nil + ) + else { + // Already diagnosed in the called method through the diagnosticsEngine. + throw ExitCode.failure + } + + configuration = effectiveConfiguration + } else { + configuration = Configuration() } - switch frontend.dumpedConfiguration { - case .success(let configuration): - print(configuration) - case .failure(let error): - throw error + do { + let configurationAsJson = try configuration.asJsonString() + print(configurationAsJson) + } catch { + diagnosticsEngine.emitError("\(error.localizedDescription)") + throw ExitCode.failure } } } From 11d6de85e6eb3aefb597dddf62008466937a5e85 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Fri, 14 Feb 2025 17:34:28 +0100 Subject: [PATCH 4/4] Incorporate review comments; Allow API breakage --- Sources/swift-format/Subcommands/DumpConfiguration.swift | 3 +-- api-breakages.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/swift-format/Subcommands/DumpConfiguration.swift b/Sources/swift-format/Subcommands/DumpConfiguration.swift index 0014151a4..3104ed588 100644 --- a/Sources/swift-format/Subcommands/DumpConfiguration.swift +++ b/Sources/swift-format/Subcommands/DumpConfiguration.swift @@ -63,8 +63,7 @@ extension SwiftFormatCommand { } do { - let configurationAsJson = try configuration.asJsonString() - print(configurationAsJson) + print(try configuration.asJsonString()) } catch { diagnosticsEngine.emitError("\(error.localizedDescription)") throw ExitCode.failure diff --git a/api-breakages.txt b/api-breakages.txt index 987ca864c..07f9a3bc2 100644 --- a/api-breakages.txt +++ b/api-breakages.txt @@ -2,3 +2,4 @@ --- API breakage: constructor FileIterator.init(urls:followSymlinks:) has been removed +API breakage: enumelement SwiftFormatError.configurationDumpFailed has been added as a new enum case