diff --git a/Documentation/RuleDocumentation.md b/Documentation/RuleDocumentation.md index f0d7e6b2..ecb376ec 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 25a77f4c..1af06a12 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 d04ef4a4..da3a34db 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 { @@ -515,10 +510,14 @@ fileprivate extension URL { } extension Configuration.RuleSeverity { - var findingSeverity: Finding.Severity { + 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 c4fe886a..987a9e8d 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 416bca6c..882b87c5 100644 --- a/Sources/SwiftFormat/API/FindingCategorizing.swift +++ b/Sources/SwiftFormat/API/FindingCategorizing.swift @@ -21,19 +21,9 @@ 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 - } -} diff --git a/Sources/SwiftFormat/Core/Context.swift b/Sources/SwiftFormat/Core/Context.swift index e00e38b2..ef56a5f5 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 2b0e97b8..9489fe9d 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 ef34b4fc..fbd85e55 100644 --- a/Sources/SwiftFormat/Core/Rule.swift +++ b/Sources/SwiftFormat/Core/Rule.swift @@ -86,7 +86,7 @@ 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 +100,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 117508f0..e9eac230 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 d5c9c9ba..f963d01e 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 8f8f5b9d..38910c07 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,19 @@ 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 9e9f9e5d..6c19e93c 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 c153c0ae..b2198fa4 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 62a83b15..2ab20825 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) } } @@ -368,6 +368,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( @@ -508,3 +509,29 @@ 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 a3c59372..cf599d13 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 00000000..916f3176 --- /dev/null +++ b/Sources/generate-swift-format/PrettyPrintCollector.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// 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 3994f5b3..e26a0226 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 ea40bcd1..221aaba1 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 220a2a23..6c8565bc 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 572b30c9..7c96a990 100644 --- a/Tests/SwiftFormatTests/API/ConfigurationTests.swift +++ b/Tests/SwiftFormatTests/API/ConfigurationTests.swift @@ -19,14 +19,10 @@ final class ConfigurationTests: XCTestCase { } func testSeverityDecoding() { - var config = Configuration() - config.ruleSeverity["AlwaysUseLowerCamelCase"] = .warning - config.ruleSeverity["AmbiguousTrailingClosureOverload"] = .error - let dictionaryData = """ { - "ruleSeverity": { + "rules": { "AlwaysUseLowerCamelCase": "warning", "AmbiguousTrailingClosureOverload": "error", } @@ -36,7 +32,8 @@ final class ConfigurationTests: XCTestCase { let jsonConfig = 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 4a707e77..99045cc0 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 88e425ac..029667e2 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 dc69fbef..4fcf8531 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/SeverityOverrideTest.swift b/Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift new file mode 100644 index 00000000..d6f6e8f6 --- /dev/null +++ b/Tests/SwiftFormatTests/Rules/SeverityOverrideTest.swift @@ -0,0 +1,113 @@ +@_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: [] + ) + } +}