From 22557b7f14781247af5b9985baf7bfd8210be83c Mon Sep 17 00:00:00 2001 From: Bernd Kolb Date: Thu, 21 Nov 2024 16:54:09 +0100 Subject: [PATCH 1/3] Allow to customize Rule severity In order to customize the severity of rules, I added the possibility to do so via the configuration files. If no severity is specified, we use the one pre-determined by the Rule itself. Example: ``` { "ruleSeverity": { "AlwaysUseLowerCamelCase": "warning", "AmbiguousTrailingClosureOverload": "error", } } ``` Issue: #879 --- .../API/Configuration+Default.swift | 1 + Sources/SwiftFormat/API/Configuration.swift | 13 +++++++ Sources/SwiftFormat/Core/Rule.swift | 12 ++++++ .../Core/RuleBasedFindingCategory.swift | 4 ++ .../DiagnosingTestCase.swift | 12 ++++++ .../_SwiftFormatTestSupport/FindingSpec.swift | 8 +++- .../API/ConfigurationTests.swift | 21 +++++++++++ .../Rules/OmitReturnsTests.swift | 12 +++--- .../TypeNamesShouldBeCapitalizedTests.swift | 37 ++++++++++--------- 9 files changed, 95 insertions(+), 25 deletions(-) diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 1af06a121..25a77f4cd 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -22,6 +22,7 @@ extension Configuration { /// the JSON will be populated from this default configuration. public init() { self.rules = Self.defaultRuleEnablements + self.ruleSeverity = [:] self.maximumBlankLines = 1 self.lineLength = 100 self.tabWidth = 8 diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 70ac916aa..f1d9e5b83 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -24,6 +24,11 @@ internal let highestSupportedConfigurationVersion = 1 /// Holds the complete set of configured values and defaults. public struct Configuration: Codable, Equatable { + public enum RuleSeverity: String, Codable, CaseIterable, Equatable, Sendable { + case warning = "warning" + case error = "error" + } + private enum CodingKeys: CodingKey { case version case maximumBlankLines @@ -42,6 +47,7 @@ public struct Configuration: Codable, Equatable { case fileScopedDeclarationPrivacy case indentSwitchCaseLabels case rules + case ruleSeverity case spacesAroundRangeFormationOperators case noAssignmentInExpressions case multiElementCollectionTrailingCommas @@ -64,6 +70,10 @@ public struct Configuration: Codable, Equatable { /// marked as `false`, or if it is missing from the dictionary. public var rules: [String: Bool] + /// The dictionary containing the severities for the rule names that we wish to run on. If a rule + /// is not listed here, the default severity is used. + public var ruleSeverity: [String: RuleSeverity] + /// The maximum number of consecutive blank lines that may appear in a file. public var maximumBlankLines: Int @@ -390,6 +400,9 @@ public struct Configuration: Codable, Equatable { self.rules = try container.decodeIfPresent([String: Bool].self, forKey: .rules) ?? defaults.rules + + self.ruleSeverity = + try container.decodeIfPresent([String: RuleSeverity].self, forKey: .ruleSeverity) ?? [:] } public func encode(to encoder: Encoder) throws { diff --git a/Sources/SwiftFormat/Core/Rule.swift b/Sources/SwiftFormat/Core/Rule.swift index 368c7087e..190e93b7e 100644 --- a/Sources/SwiftFormat/Core/Rule.swift +++ b/Sources/SwiftFormat/Core/Rule.swift @@ -86,6 +86,8 @@ extension Rule { syntaxLocation = nil } + let severity: Finding.Severity? = severity ?? context.configuration.findingSeverity(for: type(of: self)) + let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity) context.findingEmitter.emit( message, @@ -95,3 +97,13 @@ extension Rule { ) } } + +extension Configuration { + func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? { + guard let severity = self.ruleSeverity[rule.ruleName] else { return nil } + switch severity { + case .warning: return .warning + case .error: return .error + } + } +} diff --git a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift index a43dade37..42521b236 100644 --- a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift +++ b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift @@ -24,6 +24,10 @@ struct RuleBasedFindingCategory: FindingCategorizing { var severity: Finding.Severity? + public var defaultSeverity: Finding.Severity { + return severity ?? .warning + } + /// Creates a finding category that wraps the given rule type. init(ruleType: Rule.Type, severity: Finding.Severity? = nil) { self.ruleType = ruleType diff --git a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift index cb4e07267..ec560fb5f 100644 --- a/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift +++ b/Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift @@ -145,6 +145,18 @@ open class DiagnosingTestCase: XCTestCase { line: line ) } + + XCTAssertEqual( + matchedFinding.severity, + findingSpec.severity, + """ + Finding emitted at marker '\(findingSpec.marker)' \ + (line:col \(markerLocation.line):\(markerLocation.column), offset \(utf8Offset)) \ + had the wrong severity + """, + file: file, + line: line + ) } private func assertAndRemoveNote( diff --git a/Sources/_SwiftFormatTestSupport/FindingSpec.swift b/Sources/_SwiftFormatTestSupport/FindingSpec.swift index e9751ede4..6691d810c 100644 --- a/Sources/_SwiftFormatTestSupport/FindingSpec.swift +++ b/Sources/_SwiftFormatTestSupport/FindingSpec.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import SwiftFormat + /// A description of a `Finding` that can be asserted during tests. public struct FindingSpec { /// The marker that identifies the finding. @@ -21,11 +23,15 @@ public struct FindingSpec { /// A description of a `Note` that should be associated with this finding. public var notes: [NoteSpec] + /// A description of a `Note` that should be associated with this finding. + public var severity: Finding.Severity + /// Creates a new `FindingSpec` with the given values. - public init(_ marker: String = "1️⃣", message: String, notes: [NoteSpec] = []) { + public init(_ marker: String = "1️⃣", message: String, notes: [NoteSpec] = [], severity: Finding.Severity = .warning) { self.marker = marker self.message = message self.notes = notes + self.severity = severity } } diff --git a/Tests/SwiftFormatTests/API/ConfigurationTests.swift b/Tests/SwiftFormatTests/API/ConfigurationTests.swift index 86a9d8dd2..572b30c9a 100644 --- a/Tests/SwiftFormatTests/API/ConfigurationTests.swift +++ b/Tests/SwiftFormatTests/API/ConfigurationTests.swift @@ -18,6 +18,27 @@ final class ConfigurationTests: XCTestCase { XCTAssertEqual(defaultInitConfig, emptyJSONConfig) } + func testSeverityDecoding() { + var config = Configuration() + config.ruleSeverity["AlwaysUseLowerCamelCase"] = .warning + config.ruleSeverity["AmbiguousTrailingClosureOverload"] = .error + + let dictionaryData = + """ + { + "ruleSeverity": { + "AlwaysUseLowerCamelCase": "warning", + "AmbiguousTrailingClosureOverload": "error", + } + } + """.data(using: .utf8)! + let jsonDecoder = JSONDecoder() + let jsonConfig = + try! jsonDecoder.decode(Configuration.self, from: dictionaryData) + + XCTAssertEqual(config, jsonConfig) + } + func testMissingConfigurationFile() throws { #if os(Windows) #if compiler(<6.0.2) diff --git a/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift b/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift index ae0d84188..e30bddc64 100644 --- a/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift @@ -16,7 +16,7 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression") + FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring) ] ) } @@ -35,7 +35,7 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression") + FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring) ] ) } @@ -76,8 +76,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"), - FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression"), + FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), + FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), ] ) } @@ -114,8 +114,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"), - FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression"), + FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), + FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), ] ) } diff --git a/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift b/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift index fa30409e4..bd6445668 100644 --- a/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift +++ b/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift @@ -17,11 +17,11 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'"), - FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'"), - FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'"), - FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'"), - FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'"), + FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'", severity: .convention), + FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'", severity: .convention), + FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'", severity: .convention), + FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'", severity: .convention), + FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'", severity: .convention), ] ) } @@ -36,8 +36,8 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { distributed actor DistGreeter {} """, findings: [ - FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'"), - FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'"), + FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'", severity: .convention), + FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'", severity: .convention), ] ) } @@ -63,9 +63,9 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'"), - FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'"), - FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'"), + FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'", severity: .convention), + FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'", severity: .convention), + FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'", severity: .convention), ] ) } @@ -107,17 +107,18 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { distributed actor __InternalGreeter {} """, findings: [ - FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'"), - FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'"), - FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'"), - FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'"), + FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'", severity: .convention), + FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'", severity: .convention), + FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'", severity: .convention), + FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'", severity: .convention), FindingSpec( "5️⃣", - message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'" + message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'", + severity: .convention ), - FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'"), - FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'"), - FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'"), + FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'", severity: .convention), + FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'", severity: .convention), + FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'", severity: .convention), ] ) } From 923f1b99d559160de361d5c45a8380e24e8b844c Mon Sep 17 00:00:00 2001 From: Bernd Kolb Date: Fri, 22 Nov 2024 15:17:04 +0100 Subject: [PATCH 2/3] Allow to customize Severity for formating rules In order to check during CI for propper formatting, it is now also possible to specify severity for the formating rules. Issue: #879 --- .../SwiftFormat/API/FindingCategorizing.swift | 18 ++++++++++++++---- Sources/SwiftFormat/Core/FindingEmitter.swift | 5 +++-- Sources/SwiftFormat/Core/Rule.swift | 8 +++----- .../Core/RuleBasedFindingCategory.swift | 11 +++++++++-- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 3 ++- .../PrettyPrintFindingCategory.swift | 6 ++++++ .../WhitespaceFindingCategory.swift | 4 ++++ .../PrettyPrint/WhitespaceLinter.swift | 3 ++- 8 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Sources/SwiftFormat/API/FindingCategorizing.swift b/Sources/SwiftFormat/API/FindingCategorizing.swift index 46ad563ff..416bca6ca 100644 --- a/Sources/SwiftFormat/API/FindingCategorizing.swift +++ b/Sources/SwiftFormat/API/FindingCategorizing.swift @@ -17,13 +17,23 @@ /// to be displayed as part of the diagnostic message when the finding is presented to the user. /// For example, the category `Indentation` in the message `[Indentation] Indent by 2 spaces`. public protocol FindingCategorizing: CustomStringConvertible { - /// The default severity of findings emitted in this category. + /// The severity of findings emitted in this category. /// - /// By default, all findings are warnings. Individual categories may choose to override this to + /// By default, all findings are warnings. Individual categories or configuration may choose to override this to /// make the findings in those categories more severe. - var defaultSeverity: Finding.Severity { get } + func severity(configuration: Configuration) -> Finding.Severity + + /// The name of the category. + var name: String {get} } extension FindingCategorizing { - public var defaultSeverity: Finding.Severity { .warning } + func severity(configuration: Configuration) -> Finding.Severity { + return severityFromConfig(configuration: configuration) + } + + func severityFromConfig(configuration: Configuration) -> Finding.Severity { + guard let customSeverity = configuration.ruleSeverity[self.name] else { return .warning } + return customSeverity.findingSeverity + } } diff --git a/Sources/SwiftFormat/Core/FindingEmitter.swift b/Sources/SwiftFormat/Core/FindingEmitter.swift index b42e101f1..2b0e97b8a 100644 --- a/Sources/SwiftFormat/Core/FindingEmitter.swift +++ b/Sources/SwiftFormat/Core/FindingEmitter.swift @@ -44,7 +44,8 @@ final class FindingEmitter { _ message: Finding.Message, category: FindingCategorizing, location: Finding.Location? = nil, - notes: [Finding.Note] = [] + notes: [Finding.Note] = [], + context: Context ) { guard let consumer = self.consumer else { return } @@ -54,7 +55,7 @@ final class FindingEmitter { Finding( category: category, message: message, - severity: category.defaultSeverity, + severity: category.severity(configuration: context.configuration), location: location, notes: notes ) diff --git a/Sources/SwiftFormat/Core/Rule.swift b/Sources/SwiftFormat/Core/Rule.swift index 190e93b7e..ef34b4fcb 100644 --- a/Sources/SwiftFormat/Core/Rule.swift +++ b/Sources/SwiftFormat/Core/Rule.swift @@ -93,7 +93,8 @@ extension Rule { message, category: category, location: syntaxLocation.flatMap(Finding.Location.init), - notes: notes + notes: notes, + context: context ) } } @@ -101,9 +102,6 @@ extension Rule { extension Configuration { func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? { guard let severity = self.ruleSeverity[rule.ruleName] else { return nil } - switch severity { - case .warning: return .warning - case .error: return .error - } + return severity.findingSeverity } } diff --git a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift index 42521b236..117508f09 100644 --- a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift +++ b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift @@ -24,8 +24,8 @@ struct RuleBasedFindingCategory: FindingCategorizing { var severity: Finding.Severity? - public var defaultSeverity: Finding.Severity { - return severity ?? .warning + var name: String { + return description } /// Creates a finding category that wraps the given rule type. @@ -33,4 +33,11 @@ struct RuleBasedFindingCategory: FindingCategorizing { self.ruleType = ruleType self.severity = severity } + + func severity(configuration: Configuration) -> Finding.Severity { + if let severity = severity { + return severity + } + return severityFromConfig(configuration: configuration) + } } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 607364ee8..8f8f5b9d1 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -819,7 +819,8 @@ public class PrettyPrinter { context.findingEmitter.emit( message, category: category, - location: Finding.Location(file: context.fileURL.path, line: outputBuffer.lineNumber, column: column) + location: Finding.Location(file: context.fileURL.path, line: outputBuffer.lineNumber, column: column), + context: context ) } } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift index ee81342f0..9e9f9e5df 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift @@ -12,6 +12,7 @@ /// Categories for findings emitted by the pretty printer. enum PrettyPrintFindingCategory: FindingCategorizing { + /// Finding related to an end-of-line comment. case endOfLineComment @@ -24,4 +25,9 @@ enum PrettyPrintFindingCategory: FindingCategorizing { case .trailingComma: return "TrailingComma" } } + + var name: String { + self.description + } + } diff --git a/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift b/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift index bc50fb38f..c153c0aea 100644 --- a/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift +++ b/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift @@ -44,4 +44,8 @@ enum WhitespaceFindingCategory: FindingCategorizing { case .lineLength: return "LineLength" } } + + var name: String { + return self.description + } } diff --git a/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift b/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift index 30f733952..f91549e55 100644 --- a/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift +++ b/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift @@ -380,7 +380,8 @@ public class WhitespaceLinter { context.findingEmitter.emit( message, category: category, - location: Finding.Location(sourceLocation) + location: Finding.Location(sourceLocation), + context: context ) } From d80c57cf40654b5e7895b15c417dd2e32d7f1cb0 Mon Sep 17 00:00:00 2001 From: Bernd Kolb Date: Mon, 25 Nov 2024 00:19:44 +0100 Subject: [PATCH 3/3] Allow to customize Rule severity In order to customize the severity of rules, I added the possibility to do so via the configuration files. If no severity is specified, we use the one pre-determined by the Rule itself. Example: ``` { "rules": { "AlwaysUseLowerCamelCase": "warning", "AmbiguousTrailingClosureOverload": "error", "UseLetInEveryBoundCaseVariable": "true", // use rule default "UseWhereClausesInForLoops": "false", // disabled } } ``` In addition, one can now control how pretty-print violations should be treated in the same way Example: ``` { "rules": { "TrailingComma": "warning", "LineLength": "error", "Indentation": "true", // use rule default "TrailingWhitespace": "false", // disabled } } ``` Issue: #879 --- .gitignore | 2 +- Documentation/RuleDocumentation.md | 2 +- .../API/Configuration+Default.swift | 1 - Sources/SwiftFormat/API/Configuration.swift | 28 +++-- Sources/SwiftFormat/API/Finding.swift | 1 + .../SwiftFormat/API/FindingCategorizing.swift | 15 +-- Sources/SwiftFormat/Core/Context.swift | 7 +- Sources/SwiftFormat/Core/FindingEmitter.swift | 2 +- Sources/SwiftFormat/Core/Rule.swift | 9 +- .../Core/RuleBasedFindingCategory.swift | 11 +- .../Core/RuleRegistry+Generated.swift | 97 ++++++++------- .../SwiftFormat/PrettyPrint/PrettyPrint.swift | 24 +++- .../PrettyPrintFindingCategory.swift | 11 +- .../WhitespaceFindingCategory.swift | 26 ++-- .../PrettyPrint/WhitespaceLinter.swift | 42 +++++-- .../Configuration+Testing.swift | 20 +++ .../PrettyPrintCollector.swift | 102 +++++++++++++++ .../RuleRegistryGenerator.swift | 22 +++- Sources/generate-swift-format/main.swift | 9 +- .../Utilities/DiagnosticsEngine.swift | 1 + .../API/ConfigurationTests.swift | 23 ++-- .../PrettyPrint/WhitespaceTestCase.swift | 3 +- .../FileScopedDeclarationPrivacyTests.swift | 2 +- .../Rules/LintOrFormatRuleTestCase.swift | 11 +- .../Rules/OmitReturnsTests.swift | 36 +++++- .../Rules/SeverityOverrideTest.swift | 117 ++++++++++++++++++ .../TypeNamesShouldBeCapitalizedTests.swift | 102 ++++++++++++--- 27 files changed, 572 insertions(+), 154 deletions(-) create mode 100644 Sources/generate-swift-format/PrettyPrintCollector.swift create mode 100644 Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift diff --git a/.gitignore b/.gitignore index adce277b6..8a2788c69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .swiftpm/ swift-format.xcodeproj/ Package.resolved - +.index-build diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index f0d7e6b2a..ecb376ec1 100644 --- a/Documentation/RuleDocumentation.md +++ b/Documentation/RuleDocumentation.md @@ -4,7 +4,7 @@ Use the rules below in the `rules` block of your `.swift-format` configuration file, as described in -[Configuration](Configuration.md). All of these rules can be +[Configuration](Documentation/Configuration.md). All of these rules can be applied in the linter, but only some of them can format your source code automatically. diff --git a/Sources/SwiftFormat/API/Configuration+Default.swift b/Sources/SwiftFormat/API/Configuration+Default.swift index 25a77f4cd..1af06a121 100644 --- a/Sources/SwiftFormat/API/Configuration+Default.swift +++ b/Sources/SwiftFormat/API/Configuration+Default.swift @@ -22,7 +22,6 @@ extension Configuration { /// the JSON will be populated from this default configuration. public init() { self.rules = Self.defaultRuleEnablements - self.ruleSeverity = [:] self.maximumBlankLines = 1 self.lineLength = 100 self.tabWidth = 8 diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index f1d9e5b83..8cd952e74 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -27,6 +27,8 @@ public struct Configuration: Codable, Equatable { public enum RuleSeverity: String, Codable, CaseIterable, Equatable, Sendable { case warning = "warning" case error = "error" + case ruleDefault = "true" + case disabled = "false" } private enum CodingKeys: CodingKey { @@ -59,7 +61,7 @@ public struct Configuration: Codable, Equatable { /// names. /// /// This value is generated by `generate-swift-format` based on the `isOptIn` value of each rule. - public static let defaultRuleEnablements: [String: Bool] = RuleRegistry.rules + public static let defaultRuleEnablements: [String: Configuration.RuleSeverity] = RuleRegistry.rules /// The version of this configuration. private var version: Int = highestSupportedConfigurationVersion @@ -68,11 +70,7 @@ public struct Configuration: Codable, Equatable { /// The dictionary containing the rule names that we wish to run on. A rule is not used if it is /// marked as `false`, or if it is missing from the dictionary. - public var rules: [String: Bool] - - /// The dictionary containing the severities for the rule names that we wish to run on. If a rule - /// is not listed here, the default severity is used. - public var ruleSeverity: [String: RuleSeverity] + public var rules: [String: Configuration.RuleSeverity] /// The maximum number of consecutive blank lines that may appear in a file. public var maximumBlankLines: Int @@ -398,11 +396,8 @@ public struct Configuration: Codable, Equatable { // default-initialized. To get an empty rules dictionary, one can explicitly // set the `rules` key to `{}`. self.rules = - try container.decodeIfPresent([String: Bool].self, forKey: .rules) + try container.decodeIfPresent([String: Configuration.RuleSeverity].self, forKey: .rules) ?? defaults.rules - - self.ruleSeverity = - try container.decodeIfPresent([String: RuleSeverity].self, forKey: .ruleSeverity) ?? [:] } public func encode(to encoder: Encoder) throws { @@ -499,3 +494,16 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable { public init() {} } + +extension Configuration.RuleSeverity { + func findingSeverity(ruleDefault: Finding.Severity) -> Finding.Severity { + switch self { + case .warning: return .warning + case .error: return .error + case .ruleDefault: + return ruleDefault + case .disabled: + return .disabled + } + } +} diff --git a/Sources/SwiftFormat/API/Finding.swift b/Sources/SwiftFormat/API/Finding.swift index c4fe886ab..987a9e8d2 100644 --- a/Sources/SwiftFormat/API/Finding.swift +++ b/Sources/SwiftFormat/API/Finding.swift @@ -18,6 +18,7 @@ public struct Finding { case error case refactoring case convention + case disabled } /// The file path and location in that file where a finding was encountered. diff --git a/Sources/SwiftFormat/API/FindingCategorizing.swift b/Sources/SwiftFormat/API/FindingCategorizing.swift index 416bca6ca..1cd674f50 100644 --- a/Sources/SwiftFormat/API/FindingCategorizing.swift +++ b/Sources/SwiftFormat/API/FindingCategorizing.swift @@ -21,19 +21,8 @@ public protocol FindingCategorizing: CustomStringConvertible { /// /// By default, all findings are warnings. Individual categories or configuration may choose to override this to /// make the findings in those categories more severe. - func severity(configuration: Configuration) -> Finding.Severity + var severity: Finding.Severity { get } /// The name of the category. - var name: String {get} -} - -extension FindingCategorizing { - func severity(configuration: Configuration) -> Finding.Severity { - return severityFromConfig(configuration: configuration) - } - - func severityFromConfig(configuration: Configuration) -> Finding.Severity { - guard let customSeverity = configuration.ruleSeverity[self.name] else { return .warning } - return customSeverity.findingSeverity - } + var name: String { get } } diff --git a/Sources/SwiftFormat/Core/Context.swift b/Sources/SwiftFormat/Core/Context.swift index e00e38b20..12a0676cb 100644 --- a/Sources/SwiftFormat/Core/Context.swift +++ b/Sources/SwiftFormat/Core/Context.swift @@ -108,7 +108,12 @@ public final class Context { let ruleName = ruleNameCache[ObjectIdentifier(rule)] ?? R.ruleName switch ruleMask.ruleState(ruleName, at: loc) { case .default: - return configuration.rules[ruleName] ?? false + guard let configSeverity = configuration.rules[ruleName] else { return false } + if case .disabled = configSeverity { + return false + } else { + return true + } case .disabled: return false } diff --git a/Sources/SwiftFormat/Core/FindingEmitter.swift b/Sources/SwiftFormat/Core/FindingEmitter.swift index 2b0e97b8a..9489fe9da 100644 --- a/Sources/SwiftFormat/Core/FindingEmitter.swift +++ b/Sources/SwiftFormat/Core/FindingEmitter.swift @@ -55,7 +55,7 @@ final class FindingEmitter { Finding( category: category, message: message, - severity: category.severity(configuration: context.configuration), + severity: category.severity, location: location, notes: notes ) diff --git a/Sources/SwiftFormat/Core/Rule.swift b/Sources/SwiftFormat/Core/Rule.swift index ef34b4fcb..a6b891470 100644 --- a/Sources/SwiftFormat/Core/Rule.swift +++ b/Sources/SwiftFormat/Core/Rule.swift @@ -86,7 +86,8 @@ extension Rule { syntaxLocation = nil } - let severity: Finding.Severity? = severity ?? context.configuration.findingSeverity(for: type(of: self)) + let severity: Finding.Severity = + severity ?? context.configuration.findingSeverity(for: type(of: self), defaultSeverity: .warning) let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity) context.findingEmitter.emit( @@ -100,8 +101,8 @@ extension Rule { } extension Configuration { - func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? { - guard let severity = self.ruleSeverity[rule.ruleName] else { return nil } - return severity.findingSeverity + func findingSeverity(for rule: any Rule.Type, defaultSeverity: Finding.Severity) -> Finding.Severity { + guard let severity = self.rules[rule.ruleName] else { return defaultSeverity } + return severity.findingSeverity(ruleDefault: defaultSeverity) } } diff --git a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift index 117508f09..e9eac230f 100644 --- a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift +++ b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift @@ -22,22 +22,15 @@ struct RuleBasedFindingCategory: FindingCategorizing { var description: String { ruleType.ruleName } - var severity: Finding.Severity? + var severity: Finding.Severity var name: String { return description } /// Creates a finding category that wraps the given rule type. - init(ruleType: Rule.Type, severity: Finding.Severity? = nil) { + init(ruleType: Rule.Type, severity: Finding.Severity) { self.ruleType = ruleType self.severity = severity } - - func severity(configuration: Configuration) -> Finding.Severity { - if let severity = severity { - return severity - } - return severityFromConfig(configuration: configuration) - } } diff --git a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift index d5c9c9ba1..f963d01e1 100644 --- a/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift +++ b/Sources/SwiftFormat/Core/RuleRegistry+Generated.swift @@ -13,49 +13,58 @@ // This file is automatically generated with generate-swift-format. Do not edit! @_spi(Internal) public enum RuleRegistry { - public static let rules: [String: Bool] = [ - "AllPublicDeclarationsHaveDocumentation": false, - "AlwaysUseLiteralForEmptyCollectionInit": false, - "AlwaysUseLowerCamelCase": true, - "AmbiguousTrailingClosureOverload": true, - "AvoidRetroactiveConformances": true, - "BeginDocumentationCommentWithOneLineSummary": false, - "DoNotUseSemicolons": true, - "DontRepeatTypeInStaticProperties": true, - "FileScopedDeclarationPrivacy": true, - "FullyIndirectEnum": true, - "GroupNumericLiterals": true, - "IdentifiersMustBeASCII": true, - "NeverForceUnwrap": false, - "NeverUseForceTry": false, - "NeverUseImplicitlyUnwrappedOptionals": false, - "NoAccessLevelOnExtensionDeclaration": true, - "NoAssignmentInExpressions": true, - "NoBlockComments": true, - "NoCasesWithOnlyFallthrough": true, - "NoEmptyLinesOpeningClosingBraces": false, - "NoEmptyTrailingClosureParentheses": true, - "NoLabelsInCasePatterns": true, - "NoLeadingUnderscores": false, - "NoParensAroundConditions": true, - "NoPlaygroundLiterals": true, - "NoVoidReturnOnFunctionSignature": true, - "OmitExplicitReturns": false, - "OneCasePerLine": true, - "OneVariableDeclarationPerLine": true, - "OnlyOneTrailingClosureArgument": true, - "OrderedImports": true, - "ReplaceForEachWithForLoop": true, - "ReturnVoidInsteadOfEmptyTuple": true, - "TypeNamesShouldBeCapitalized": true, - "UseEarlyExits": false, - "UseExplicitNilCheckInConditions": true, - "UseLetInEveryBoundCaseVariable": true, - "UseShorthandTypeNames": true, - "UseSingleLinePropertyGetter": true, - "UseSynthesizedInitializer": true, - "UseTripleSlashForDocumentationComments": true, - "UseWhereClausesInForLoops": false, - "ValidateDocumentationComments": false, + public static let rules: [String: Configuration.RuleSeverity] = [ + "AllPublicDeclarationsHaveDocumentation": .disabled, + "AlwaysUseLiteralForEmptyCollectionInit": .disabled, + "AlwaysUseLowerCamelCase": .ruleDefault, + "AmbiguousTrailingClosureOverload": .ruleDefault, + "AvoidRetroactiveConformances": .ruleDefault, + "BeginDocumentationCommentWithOneLineSummary": .disabled, + "DoNotUseSemicolons": .ruleDefault, + "DontRepeatTypeInStaticProperties": .ruleDefault, + "FileScopedDeclarationPrivacy": .ruleDefault, + "FullyIndirectEnum": .ruleDefault, + "GroupNumericLiterals": .ruleDefault, + "IdentifiersMustBeASCII": .ruleDefault, + "NeverForceUnwrap": .disabled, + "NeverUseForceTry": .disabled, + "NeverUseImplicitlyUnwrappedOptionals": .disabled, + "NoAccessLevelOnExtensionDeclaration": .ruleDefault, + "NoAssignmentInExpressions": .ruleDefault, + "NoBlockComments": .ruleDefault, + "NoCasesWithOnlyFallthrough": .ruleDefault, + "NoEmptyLinesOpeningClosingBraces": .disabled, + "NoEmptyTrailingClosureParentheses": .ruleDefault, + "NoLabelsInCasePatterns": .ruleDefault, + "NoLeadingUnderscores": .disabled, + "NoParensAroundConditions": .ruleDefault, + "NoPlaygroundLiterals": .ruleDefault, + "NoVoidReturnOnFunctionSignature": .ruleDefault, + "OmitExplicitReturns": .disabled, + "OneCasePerLine": .ruleDefault, + "OneVariableDeclarationPerLine": .ruleDefault, + "OnlyOneTrailingClosureArgument": .ruleDefault, + "OrderedImports": .ruleDefault, + "ReplaceForEachWithForLoop": .ruleDefault, + "ReturnVoidInsteadOfEmptyTuple": .ruleDefault, + "TypeNamesShouldBeCapitalized": .ruleDefault, + "UseEarlyExits": .disabled, + "UseExplicitNilCheckInConditions": .ruleDefault, + "UseLetInEveryBoundCaseVariable": .ruleDefault, + "UseShorthandTypeNames": .ruleDefault, + "UseSingleLinePropertyGetter": .ruleDefault, + "UseSynthesizedInitializer": .ruleDefault, + "UseTripleSlashForDocumentationComments": .ruleDefault, + "UseWhereClausesInForLoops": .disabled, + "ValidateDocumentationComments": .disabled, + "AddLines": .ruleDefault, + "EndOfLineComment": .ruleDefault, + "Indentation": .ruleDefault, + "LineLength": .ruleDefault, + "RemoveLine": .ruleDefault, + "Spacing": .ruleDefault, + "SpacingCharacter": .ruleDefault, + "TrailingComma": .ruleDefault, + "TrailingWhitespace": .ruleDefault, ] } diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift index 8f8f5b9d1..4c9dc4bc9 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift @@ -475,7 +475,7 @@ public class PrettyPrinter { if wasEndOfLine { if !(canFit(comment.length) || isBreakingSuppressed) { - diagnose(.moveEndOfLineComment, category: .endOfLineComment) + diagnose(.moveEndOfLineComment, category: .endOfLineComment().withSeverity(configuration)) } } outputBuffer.write(comment.print(indent: currentIndentation)) @@ -515,9 +515,9 @@ public class PrettyPrinter { startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement && configuration.multiElementCollectionTrailingCommas if shouldHaveTrailingComma && !hasTrailingComma { - diagnose(.addTrailingComma, category: .trailingComma) + diagnose(.addTrailingComma, category: .trailingComma().withSeverity(configuration)) } else if !shouldHaveTrailingComma && hasTrailingComma { - diagnose(.removeTrailingComma, category: .trailingComma) + diagnose(.removeTrailingComma, category: .trailingComma().withSeverity(configuration)) } let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma @@ -814,6 +814,7 @@ public class PrettyPrinter { /// Emits a finding with the given message and category at the current location in `outputBuffer`. private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) { + if case .disabled = category.severity { return } // Add 1 since columns uses 1-based indices. let column = outputBuffer.column + 1 context.findingEmitter.emit( @@ -835,3 +836,20 @@ extension Finding.Message { fileprivate static let removeTrailingComma: Finding.Message = "remove trailing comma from the last element in single line collection literal" } + +extension PrettyPrintFindingCategory { + func withSeverity(_ configuration: Configuration) -> Self { + let category: PrettyPrintFindingCategory = self + let severity = + configuration + .rules[category.name]? + .findingSeverity(ruleDefault: category.severity) ?? category.severity + + switch self { + case .endOfLineComment: + return .endOfLineComment(severity) + case .trailingComma: + return .trailingComma(severity) + } + } +} diff --git a/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift b/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift index 9e9f9e5df..ac47f526d 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift @@ -14,10 +14,10 @@ enum PrettyPrintFindingCategory: FindingCategorizing { /// Finding related to an end-of-line comment. - case endOfLineComment + case endOfLineComment(Finding.Severity = .warning) /// Findings related to the presence of absence of a trailing comma in collection literals. - case trailingComma + case trailingComma(Finding.Severity = .warning) var description: String { switch self { @@ -30,4 +30,11 @@ enum PrettyPrintFindingCategory: FindingCategorizing { self.description } + var severity: Finding.Severity { + switch self { + case .endOfLineComment(let severity): return severity + case .trailingComma(let severity): return severity + } + } + } diff --git a/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift b/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift index c153c0aea..02f7d0233 100644 --- a/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift +++ b/Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift @@ -13,25 +13,25 @@ /// Categories for findings emitted by the whitespace linter. enum WhitespaceFindingCategory: FindingCategorizing { /// Findings related to trailing whitespace on a line. - case trailingWhitespace + case trailingWhitespace(Finding.Severity = .warning) /// Findings related to indentation (i.e., whitespace at the beginning of a line). - case indentation + case indentation(Finding.Severity = .warning) /// Findings related to interior whitespace (i.e., neither leading nor trailing space). - case spacing + case spacing(Finding.Severity = .warning) /// Findings related to specific characters used for interior whitespace. - case spacingCharacter + case spacingCharacter(Finding.Severity = .warning) /// Findings related to the removal of line breaks. - case removeLine + case removeLine(Finding.Severity = .warning) /// Findings related to the addition of line breaks. - case addLines + case addLines(Finding.Severity = .warning) /// Findings related to the length of a line. - case lineLength + case lineLength(Finding.Severity = .warning) var description: String { switch self { @@ -48,4 +48,16 @@ enum WhitespaceFindingCategory: FindingCategorizing { var name: String { return self.description } + + var severity: Finding.Severity { + switch self { + case .trailingWhitespace(let severity): return severity + case .indentation(let severity): return severity + case .spacing(let severity): return severity + case .spacingCharacter(let severity): return severity + case .removeLine(let severity): return severity + case .addLines(let severity): return severity + case .lineLength(let severity): return severity + } + } } diff --git a/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift b/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift index f91549e55..9a49ac98e 100644 --- a/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift +++ b/Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift @@ -131,7 +131,7 @@ public class WhitespaceLinter { // If there were excess newlines in the user input, tell the user to remove them. This // short-circuits the trailing whitespace check below; we don't bother telling the user // about trailing whitespace on a line that we're also telling them to delete. - diagnose(.removeLineError, category: .removeLine, utf8Offset: userIndex) + diagnose(.removeLineError, category: .removeLine().withSeverity(context), utf8Offset: userIndex) userIndex += userRun.count + 1 } else if runIndex != userRuns.count - 1 { if let formattedRun = possibleFormattedRun { @@ -169,7 +169,7 @@ public class WhitespaceLinter { if excessFormattedLines > 0 && !isLineTooLong { diagnose( .addLinesError(excessFormattedLines), - category: .addLines, + category: .addLines().withSeverity(context), utf8Offset: userWhitespace.startIndex ) } @@ -249,7 +249,7 @@ public class WhitespaceLinter { } isLineTooLong = true - diagnose(.lineLengthError, category: .lineLength, utf8Offset: adjustedUserIndex) + diagnose(.lineLengthError, category: .lineLength().withSeverity(context), utf8Offset: adjustedUserIndex) } /// Compare user and formatted whitespace buffers, and check for indentation errors. @@ -275,7 +275,7 @@ public class WhitespaceLinter { let expected = indentation(of: formattedRun) diagnose( .indentationError(expected: expected, actual: actual), - category: .indentation, + category: .indentation().withSeverity(context), utf8Offset: userIndex ) } @@ -292,7 +292,7 @@ public class WhitespaceLinter { formattedRun: ArraySlice ) { if userRun != formattedRun { - diagnose(.trailingWhitespaceError, category: .trailingWhitespace, utf8Offset: userIndex) + diagnose(.trailingWhitespaceError, category: .trailingWhitespace().withSeverity(context), utf8Offset: userIndex) } } @@ -316,10 +316,10 @@ public class WhitespaceLinter { // This assumes tabs will always be forbidden for inter-token spacing (but not for leading // indentation). if userRun.contains(utf8Tab) { - diagnose(.spacingCharError, category: .spacingCharacter, utf8Offset: userIndex) + diagnose(.spacingCharError, category: .spacingCharacter().withSeverity(context), utf8Offset: userIndex) } else if formattedRun.count != userRun.count { let delta = formattedRun.count - userRun.count - diagnose(.spacingError(delta), category: .spacing, utf8Offset: userIndex) + diagnose(.spacingError(delta), category: .spacing().withSeverity(context), utf8Offset: userIndex) } } @@ -375,6 +375,7 @@ public class WhitespaceLinter { category: WhitespaceFindingCategory, utf8Offset: Int ) { + if case .disabled = category.severity { return } let absolutePosition = AbsolutePosition(utf8Offset: utf8Offset) let sourceLocation = context.sourceLocationConverter.location(for: absolutePosition) context.findingEmitter.emit( @@ -515,3 +516,30 @@ extension Finding.Message { fileprivate static let lineLengthError: Finding.Message = "line is too long" } + +extension WhitespaceFindingCategory { + func withSeverity(_ context: Context) -> Self { + let category: WhitespaceFindingCategory = self + let severity = + context.configuration + .rules[category.name]? + .findingSeverity(ruleDefault: category.severity) ?? category.severity + + switch self { + case .trailingWhitespace(_): + return .trailingWhitespace(severity) + case .indentation(_): + return .indentation(severity) + case .spacing(_): + return .spacing(severity) + case .spacingCharacter(_): + return .spacingCharacter(severity) + case .removeLine(_): + return .removeLine(severity) + case .addLines(_): + return .addLines(severity) + case .lineLength(_): + return .lineLength(severity) + } + } +} diff --git a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift index a3c593726..e866fd7d1 100644 --- a/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift +++ b/Sources/_SwiftFormatTestSupport/Configuration+Testing.swift @@ -44,4 +44,24 @@ extension Configuration { config.indentBlankLines = false return config } + + public static func forTesting(enabledRule: String) -> Configuration { + var config = Configuration.forTesting.disableAllRules() + config.rules[enabledRule] = .ruleDefault + return config + } +} + +extension Configuration { + public func disableAllRules() -> Self { + var config = self + config.rules = config.rules.mapValues({ _ in .disabled }) + return config + } + + public func enable(_ rule: String, severity: Configuration.RuleSeverity) -> Self { + var config = self + config.rules[rule] = severity + return config + } } diff --git a/Sources/generate-swift-format/PrettyPrintCollector.swift b/Sources/generate-swift-format/PrettyPrintCollector.swift new file mode 100644 index 000000000..f867036a0 --- /dev/null +++ b/Sources/generate-swift-format/PrettyPrintCollector.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 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 +@_spi(Rules) import SwiftFormat +import SwiftParser +import SwiftSyntax + +/// Collects information about rules in the formatter code base. +final class PrettyPrintCollector { + + /// A list of all the format-only pretty-print categories found in the code base. + var allPrettyPrinterCategories = Set() + + /// Populates the internal collections with rules in the given directory. + /// + /// - Parameter url: The file system URL that should be scanned for rules. + func collect(from url: URL) throws { + // For each file in the Rules directory, find types that either conform to SyntaxLintRule or + // inherit from SyntaxFormatRule. + let fm = FileManager.default + guard let rulesEnumerator = fm.enumerator(atPath: url.path) else { + fatalError("Could not list the directory \(url.path)") + } + + for baseName in rulesEnumerator { + // Ignore files that aren't Swift source files. + guard let baseName = baseName as? String, baseName.hasSuffix(".swift") else { continue } + + let fileURL = url.appendingPathComponent(baseName) + let fileInput = try String(contentsOf: fileURL) + let sourceFile = Parser.parse(source: fileInput) + + for statement in sourceFile.statements { + let pp = self.detectPrettyPrintCategories(at: statement) + allPrettyPrinterCategories.formUnion(pp) + } + } + } + + private func detectPrettyPrintCategories(at statement: CodeBlockItemSyntax) -> [String] { + guard let enumDecl = statement.item.as(EnumDeclSyntax.self) else { + return [] + } + + if enumDecl.name.text == "PrettyPrintFindingCategory" { + print("HIT") + } + + // Make sure it has an inheritance clause. + guard let inheritanceClause = enumDecl.inheritanceClause else { + return [] + } + + // Scan through the inheritance clause to find one of the protocols/types we're interested in. + for inheritance in inheritanceClause.inheritedTypes { + guard let identifier = inheritance.type.as(IdentifierTypeSyntax.self) else { + continue + } + + if identifier.name.text != "FindingCategorizing" { + // Keep looking at the other inheritances. + continue + } + + // Now that we know it's a pretty printing category, collect the `description` method and extract the name. + for member in enumDecl.memberBlock.members { + guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { continue } + guard + let descriptionDecl = varDecl.bindings + .first(where: { + $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "description" + }) + else { continue } + let pp = PrettyPrintCategoryVisitor(viewMode: .sourceAccurate) + _ = pp.walk(descriptionDecl) + return pp.prettyPrintCategories + } + } + + return [] + } +} + +final class PrettyPrintCategoryVisitor: SyntaxVisitor { + + var prettyPrintCategories: [String] = [] + + override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind { + prettyPrintCategories.append(node.content.text) + return .skipChildren + } +} diff --git a/Sources/generate-swift-format/RuleRegistryGenerator.swift b/Sources/generate-swift-format/RuleRegistryGenerator.swift index 3994f5b3f..e26a02266 100644 --- a/Sources/generate-swift-format/RuleRegistryGenerator.swift +++ b/Sources/generate-swift-format/RuleRegistryGenerator.swift @@ -18,9 +18,13 @@ final class RuleRegistryGenerator: FileGenerator { /// The rules collected by scanning the formatter source code. let ruleCollector: RuleCollector + /// The pretty-printing categories collected by scanning the formatter source code. + let prettyPrintCollector: PrettyPrintCollector + /// Creates a new rule registry generator. - init(ruleCollector: RuleCollector) { + init(ruleCollector: RuleCollector, prettyPrintCollector: PrettyPrintCollector) { self.ruleCollector = ruleCollector + self.prettyPrintCollector = prettyPrintCollector } func write(into handle: FileHandle) throws { @@ -41,14 +45,26 @@ final class RuleRegistryGenerator: FileGenerator { // This file is automatically generated with generate-swift-format. Do not edit! @_spi(Internal) public enum RuleRegistry { - public static let rules: [String: Bool] = [ + public static let rules: [String: Configuration.RuleSeverity] = [ """ ) for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) { - handle.write(" \"\(detectedRule.typeName)\": \(!detectedRule.isOptIn),\n") + handle.write(" \"\(detectedRule.typeName)\": \(severity(detectedRule.isOptIn)),\n") + } + + for ppCategory in prettyPrintCollector.allPrettyPrinterCategories.sorted(by: { $0 < $1 }) { + handle.write(" \"\(ppCategory)\": .ruleDefault,\n") } handle.write(" ]\n}\n") } + + func severity(_ isOptIn: Bool) -> String { + if isOptIn { + return ".disabled" + } else { + return ".ruleDefault" + } + } } diff --git a/Sources/generate-swift-format/main.swift b/Sources/generate-swift-format/main.swift index ea40bcd1b..221aaba1b 100644 --- a/Sources/generate-swift-format/main.swift +++ b/Sources/generate-swift-format/main.swift @@ -20,6 +20,10 @@ let rulesDirectory = sourcesDirectory .appendingPathComponent("SwiftFormat") .appendingPathComponent("Rules") +let prettyPrintDirectory = + sourcesDirectory + .appendingPathComponent("SwiftFormat") + .appendingPathComponent("PrettyPrint") let pipelineFile = sourcesDirectory .appendingPathComponent("SwiftFormat") @@ -46,12 +50,15 @@ let ruleDocumentationFile = var ruleCollector = RuleCollector() try ruleCollector.collect(from: rulesDirectory) +var prettyPrintCollector = PrettyPrintCollector() +try prettyPrintCollector.collect(from: prettyPrintDirectory) + // Generate a file with extensions for the lint and format pipelines. let pipelineGenerator = PipelineGenerator(ruleCollector: ruleCollector) try pipelineGenerator.generateFile(at: pipelineFile) // Generate the rule registry dictionary for configuration. -let registryGenerator = RuleRegistryGenerator(ruleCollector: ruleCollector) +let registryGenerator = RuleRegistryGenerator(ruleCollector: ruleCollector, prettyPrintCollector: prettyPrintCollector) try registryGenerator.generateFile(at: ruleRegistryFile) // Generate the rule name cache. diff --git a/Sources/swift-format/Utilities/DiagnosticsEngine.swift b/Sources/swift-format/Utilities/DiagnosticsEngine.swift index 220a2a23c..6c8565bc3 100644 --- a/Sources/swift-format/Utilities/DiagnosticsEngine.swift +++ b/Sources/swift-format/Utilities/DiagnosticsEngine.swift @@ -140,6 +140,7 @@ final class DiagnosticsEngine { case .warning: severity = .warning case .refactoring: severity = .warning case .convention: severity = .warning + case .disabled: fatalError("must not be called for disabled findings") } return Diagnostic( severity: severity, diff --git a/Tests/SwiftFormatTests/API/ConfigurationTests.swift b/Tests/SwiftFormatTests/API/ConfigurationTests.swift index 572b30c9a..239589d3a 100644 --- a/Tests/SwiftFormatTests/API/ConfigurationTests.swift +++ b/Tests/SwiftFormatTests/API/ConfigurationTests.swift @@ -19,24 +19,21 @@ final class ConfigurationTests: XCTestCase { } func testSeverityDecoding() { - var config = Configuration() - config.ruleSeverity["AlwaysUseLowerCamelCase"] = .warning - config.ruleSeverity["AmbiguousTrailingClosureOverload"] = .error - let dictionaryData = - """ - { - "ruleSeverity": { - "AlwaysUseLowerCamelCase": "warning", - "AmbiguousTrailingClosureOverload": "error", + """ + { + "rules": { + "AlwaysUseLowerCamelCase": "warning", + "AmbiguousTrailingClosureOverload": "error", + } } - } - """.data(using: .utf8)! + """.data(using: .utf8)! let jsonDecoder = JSONDecoder() let jsonConfig = - try! jsonDecoder.decode(Configuration.self, from: dictionaryData) + try! jsonDecoder.decode(Configuration.self, from: dictionaryData) - XCTAssertEqual(config, jsonConfig) + XCTAssertEqual(jsonConfig.rules["AlwaysUseLowerCamelCase"]!, .warning) + XCTAssertEqual(jsonConfig.rules["AmbiguousTrailingClosureOverload"]!, .error) } func testMissingConfigurationFile() throws { diff --git a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift index 4a707e779..99045cc05 100644 --- a/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift +++ b/Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift @@ -21,6 +21,7 @@ class WhitespaceTestCase: DiagnosingTestCase { input: String, expected: String, linelength: Int? = nil, + configuration: Configuration = Configuration.forTesting, findings: [FindingSpec], file: StaticString = #file, line: UInt = #line @@ -28,7 +29,7 @@ class WhitespaceTestCase: DiagnosingTestCase { let markedText = MarkedText(textWithMarkers: input) let sourceFileSyntax = Parser.parse(source: markedText.textWithoutMarkers) - var configuration = Configuration.forTesting + var configuration = configuration if let linelength = linelength { configuration.lineLength = linelength } diff --git a/Tests/SwiftFormatTests/Rules/FileScopedDeclarationPrivacyTests.swift b/Tests/SwiftFormatTests/Rules/FileScopedDeclarationPrivacyTests.swift index 88e425ac6..029667e26 100644 --- a/Tests/SwiftFormatTests/Rules/FileScopedDeclarationPrivacyTests.swift +++ b/Tests/SwiftFormatTests/Rules/FileScopedDeclarationPrivacyTests.swift @@ -167,7 +167,7 @@ final class FileScopedDeclarationPrivacyTests: LintOrFormatRuleTestCase { findingsProvider: (String, String) -> [FindingSpec] ) { for testConfig in testConfigurations { - var configuration = Configuration.forTesting + var configuration = Configuration.forTesting(enabledRule: FileScopedDeclarationPrivacy.self.ruleName) configuration.fileScopedDeclarationPrivacy.accessLevel = testConfig.desired let substitutedInput = source.replacingOccurrences(of: "$access$", with: testConfig.original) diff --git a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift index dc69fbef6..4fcf85310 100644 --- a/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift +++ b/Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift @@ -37,8 +37,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { var emittedFindings = [Finding]() // Force the rule to be enabled while we test it. - var configuration = Configuration.forTesting - configuration.rules[type.ruleName] = true + let configuration = Configuration.forTesting(enabledRule: type.ruleName) let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, @@ -47,8 +46,6 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { ) var emittedPipelineFindings = [Finding]() - // Disable default rules, so only select rule runs in pipeline - configuration.rules = [type.ruleName: true] let pipeline = SwiftLinter( configuration: configuration, findingConsumer: { emittedPipelineFindings.append($0) } @@ -106,8 +103,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { var emittedFindings = [Finding]() // Force the rule to be enabled while we test it. - var configuration = configuration ?? Configuration.forTesting - configuration.rules[formatType.ruleName] = true + let configuration = configuration ?? Configuration.forTesting(enabledRule: formatType.ruleName) + let context = makeContext( sourceFileSyntax: sourceFileSyntax, configuration: configuration, @@ -150,8 +147,6 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase { ) var emittedPipelineFindings = [Finding]() - // Disable default rules, so only select rule runs in pipeline - configuration.rules = [formatType.ruleName: true] let pipeline = SwiftFormatter( configuration: configuration, findingConsumer: { emittedPipelineFindings.append($0) } diff --git a/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift b/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift index e30bddc64..521e931c8 100644 --- a/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift +++ b/Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift @@ -16,7 +16,11 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring) + FindingSpec( + "1️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ) ] ) } @@ -35,7 +39,11 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring) + FindingSpec( + "1️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ) ] ) } @@ -76,8 +84,16 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), - FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), + FindingSpec( + "1️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ), + FindingSpec( + "2️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ), ] ) } @@ -114,8 +130,16 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), - FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring), + FindingSpec( + "1️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ), + FindingSpec( + "2️⃣", + message: "'return' can be omitted because body consists of a single expression", + severity: .refactoring + ), ] ) } diff --git a/Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift b/Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift new file mode 100644 index 000000000..2e8232e16 --- /dev/null +++ b/Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift @@ -0,0 +1,117 @@ +@_spi(Rules) import SwiftFormat +import _SwiftFormatTestSupport + +final class SeverityOverrideRuleTest: LintOrFormatRuleTestCase { + func testDoNotUseSemicolonAsError() { + + var config = Configuration.forTesting.disableAllRules() + config.rules[DoNotUseSemicolons.self.ruleName] = .error + + assertFormatting( + DoNotUseSemicolons.self, + input: """ + print("hello")1️⃣; + """, + expected: """ + print("hello") + """, + findings: [ + FindingSpec("1️⃣", message: "remove ';'", severity: .error) + ], + configuration: config + ) + } + + func testDoNotUseSemicolonDisabled() { + + var config = Configuration.forTesting.disableAllRules() + config.rules[DoNotUseSemicolons.self.ruleName] = .disabled + + assertFormatting( + DoNotUseSemicolons.self, + input: """ + print("hello"); + """, + expected: """ + print("hello"); + """, + findings: [], + configuration: config + ) + } +} + +final class SeverityOverridePrettyPrintTest: PrettyPrintTestCase { + + func testTrailingCommaDiagnosticsDisabled() { + assertPrettyPrintEqual( + input: """ + let a = [1, 2, 3,] + """, + expected: """ + let a = [1, 2, 3,] + + """, + linelength: 45, + configuration: Configuration.forTesting.disableAllRules().enable("TrailingComma", severity: .disabled), + whitespaceOnly: true, + findings: [] + ) + } + + func testTrailingCommaDiagnosticsAsError() { + assertPrettyPrintEqual( + input: """ + let a = [1, 2, 31️⃣,] + """, + expected: """ + let a = [1, 2, 3,] + + """, + linelength: 45, + configuration: Configuration.forTesting.disableAllRules().enable("TrailingComma", severity: .error), + whitespaceOnly: true, + findings: [ + FindingSpec( + "1️⃣", + message: "remove trailing comma from the last element in single line collection literal", + severity: .error + ) + ] + ) + } +} + +final class SeverityOverrideWhitespaceTest: WhitespaceTestCase { + func testTrailingWhitespaceAsError() { + assertWhitespaceLint( + input: """ + let a = 1231️⃣\u{20}\u{20} + + """, + expected: """ + let a = 123 + + """, + configuration: Configuration.forTesting.disableAllRules().enable("TrailingWhitespace", severity: .error), + findings: [ + FindingSpec("1️⃣", message: "remove trailing whitespace", severity: .error) + ] + ) + } + + func testTrailingWhitespaceDisabled() { + assertWhitespaceLint( + input: """ + let a = 123\u{20}\u{20} + + """, + expected: """ + let a = 123 + + """, + configuration: Configuration.forTesting.disableAllRules().enable("TrailingWhitespace", severity: .disabled), + findings: [] + ) + } +} diff --git a/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift b/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift index bd6445668..3868645cf 100644 --- a/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift +++ b/Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift @@ -17,11 +17,31 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'", severity: .convention), - FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'", severity: .convention), - FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'", severity: .convention), - FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'", severity: .convention), - FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'", severity: .convention), + FindingSpec( + "1️⃣", + message: "rename the struct 'a' using UpperCamelCase; for example, 'A'", + severity: .convention + ), + FindingSpec( + "2️⃣", + message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'", + severity: .convention + ), + FindingSpec( + "3️⃣", + message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'", + severity: .convention + ), + FindingSpec( + "4️⃣", + message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'", + severity: .convention + ), + FindingSpec( + "5️⃣", + message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'", + severity: .convention + ), ] ) } @@ -36,8 +56,16 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { distributed actor DistGreeter {} """, findings: [ - FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'", severity: .convention), - FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'", severity: .convention), + FindingSpec( + "1️⃣", + message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'", + severity: .convention + ), + FindingSpec( + "2️⃣", + message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'", + severity: .convention + ), ] ) } @@ -63,9 +91,21 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { } """, findings: [ - FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'", severity: .convention), - FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'", severity: .convention), - FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'", severity: .convention), + FindingSpec( + "1️⃣", + message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'", + severity: .convention + ), + FindingSpec( + "2️⃣", + message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'", + severity: .convention + ), + FindingSpec( + "3️⃣", + message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'", + severity: .convention + ), ] ) } @@ -107,18 +147,46 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase { distributed actor __InternalGreeter {} """, findings: [ - FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'", severity: .convention), - FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'", severity: .convention), - FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'", severity: .convention), - FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'", severity: .convention), + FindingSpec( + "1️⃣", + message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'", + severity: .convention + ), + FindingSpec( + "2️⃣", + message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'", + severity: .convention + ), + FindingSpec( + "3️⃣", + message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'", + severity: .convention + ), + FindingSpec( + "4️⃣", + message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'", + severity: .convention + ), FindingSpec( "5️⃣", message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'", severity: .convention ), - FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'", severity: .convention), - FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'", severity: .convention), - FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'", severity: .convention), + FindingSpec( + "6️⃣", + message: "rename the enum '__e' using UpperCamelCase; for example, '__E'", + severity: .convention + ), + FindingSpec( + "7️⃣", + message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'", + severity: .convention + ), + FindingSpec( + "8️⃣", + message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'", + severity: .convention + ), ] ) }