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.swift b/Sources/SwiftFormat/API/Configuration.swift index 70ac916aa..8cd952e74 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -24,6 +24,13 @@ 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" + case ruleDefault = "true" + case disabled = "false" + } + private enum CodingKeys: CodingKey { case version case maximumBlankLines @@ -42,6 +49,7 @@ public struct Configuration: Codable, Equatable { case fileScopedDeclarationPrivacy case indentSwitchCaseLabels case rules + case ruleSeverity case spacesAroundRangeFormationOperators case noAssignmentInExpressions case multiElementCollectionTrailingCommas @@ -53,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 @@ -62,7 +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] + public var rules: [String: Configuration.RuleSeverity] /// The maximum number of consecutive blank lines that may appear in a file. public var maximumBlankLines: Int @@ -388,7 +396,7 @@ 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 } @@ -486,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 46ad563ff..1cd674f50 100644 --- a/Sources/SwiftFormat/API/FindingCategorizing.swift +++ b/Sources/SwiftFormat/API/FindingCategorizing.swift @@ -17,13 +17,12 @@ /// 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 } -} + var severity: Finding.Severity { get } -extension FindingCategorizing { - public var defaultSeverity: Finding.Severity { .warning } + /// The name of the category. + 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 b42e101f1..9489fe9da 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, location: location, notes: notes ) diff --git a/Sources/SwiftFormat/Core/Rule.swift b/Sources/SwiftFormat/Core/Rule.swift index 368c7087e..a6b891470 100644 --- a/Sources/SwiftFormat/Core/Rule.swift +++ b/Sources/SwiftFormat/Core/Rule.swift @@ -86,12 +86,23 @@ extension Rule { syntaxLocation = nil } + 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( message, category: category, location: syntaxLocation.flatMap(Finding.Location.init), - notes: notes + notes: notes, + context: context ) } } + +extension Configuration { + 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 a43dade37..e9eac230f 100644 --- a/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift +++ b/Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift @@ -22,10 +22,14 @@ 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 } 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 607364ee8..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,12 +814,14 @@ 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( 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 ) } } @@ -834,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 ee81342f0..ac47f526d 100644 --- a/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift +++ b/Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift @@ -12,11 +12,12 @@ /// Categories for findings emitted by the pretty printer. 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 { @@ -24,4 +25,16 @@ enum PrettyPrintFindingCategory: FindingCategorizing { case .trailingComma: return "TrailingComma" } } + + var name: String { + 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 bc50fb38f..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 { @@ -44,4 +44,20 @@ enum WhitespaceFindingCategory: FindingCategorizing { case .lineLength: return "LineLength" } } + + 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 30f733952..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,12 +375,14 @@ 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( message, category: category, - location: Finding.Location(sourceLocation) + location: Finding.Location(sourceLocation), + context: context ) } @@ -514,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/_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/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 86a9d8dd2..239589d3a 100644 --- a/Tests/SwiftFormatTests/API/ConfigurationTests.swift +++ b/Tests/SwiftFormatTests/API/ConfigurationTests.swift @@ -18,6 +18,24 @@ final class ConfigurationTests: XCTestCase { XCTAssertEqual(defaultInitConfig, emptyJSONConfig) } + func testSeverityDecoding() { + let dictionaryData = + """ + { + "rules": { + "AlwaysUseLowerCamelCase": "warning", + "AmbiguousTrailingClosureOverload": "error", + } + } + """.data(using: .utf8)! + let jsonDecoder = JSONDecoder() + let jsonConfig = + try! jsonDecoder.decode(Configuration.self, from: dictionaryData) + + XCTAssertEqual(jsonConfig.rules["AlwaysUseLowerCamelCase"]!, .warning) + XCTAssertEqual(jsonConfig.rules["AmbiguousTrailingClosureOverload"]!, .error) + } + func testMissingConfigurationFile() throws { #if os(Windows) #if compiler(<6.0.2) 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 ae0d84188..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") + 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") + 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"), - 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 +130,16 @@ 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/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 fa30409e4..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'"), - 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 +56,16 @@ 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 +91,21 @@ 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 +147,46 @@ 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'", + 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'"), - FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'"), - FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'"), ] ) }