Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8fdeb63

Browse files
committedFeb 4, 2025
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: swiftlang#879
1 parent 923f1b9 commit 8fdeb63

25 files changed

+441
-122
lines changed
 

‎.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
.swiftpm/
33
swift-format.xcodeproj/
44
Package.resolved
5-
5+
.index-build

‎Documentation/RuleDocumentation.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Use the rules below in the `rules` block of your `.swift-format`
66
configuration file, as described in
7-
[Configuration](Configuration.md). All of these rules can be
7+
[Configuration](Documentation/Configuration.md). All of these rules can be
88
applied in the linter, but only some of them can format your source code
99
automatically.
1010

‎Sources/SwiftFormat/API/Configuration+Default.swift

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ extension Configuration {
2222
/// the JSON will be populated from this default configuration.
2323
public init() {
2424
self.rules = Self.defaultRuleEnablements
25-
self.ruleSeverity = [:]
2625
self.maximumBlankLines = 1
2726
self.lineLength = 100
2827
self.tabWidth = 8

‎Sources/SwiftFormat/API/Configuration.swift

+19-10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public struct Configuration: Codable, Equatable {
2727
public enum RuleSeverity: String, Codable, CaseIterable, Equatable, Sendable {
2828
case warning = "warning"
2929
case error = "error"
30+
case ruleDefault = "true"
31+
case disabled = "false"
3032
}
3133

3234
private enum CodingKeys: CodingKey {
@@ -59,7 +61,7 @@ public struct Configuration: Codable, Equatable {
5961
/// names.
6062
///
6163
/// This value is generated by `generate-swift-format` based on the `isOptIn` value of each rule.
62-
public static let defaultRuleEnablements: [String: Bool] = RuleRegistry.rules
64+
public static let defaultRuleEnablements: [String: Configuration.RuleSeverity] = RuleRegistry.rules
6365

6466
/// The version of this configuration.
6567
private var version: Int = highestSupportedConfigurationVersion
@@ -68,11 +70,7 @@ public struct Configuration: Codable, Equatable {
6870

6971
/// The dictionary containing the rule names that we wish to run on. A rule is not used if it is
7072
/// marked as `false`, or if it is missing from the dictionary.
71-
public var rules: [String: Bool]
72-
73-
/// The dictionary containing the severities for the rule names that we wish to run on. If a rule
74-
/// is not listed here, the default severity is used.
75-
public var ruleSeverity: [String: RuleSeverity]
73+
public var rules: [String: Configuration.RuleSeverity]
7674

7775
/// The maximum number of consecutive blank lines that may appear in a file.
7876
public var maximumBlankLines: Int
@@ -398,11 +396,8 @@ public struct Configuration: Codable, Equatable {
398396
// default-initialized. To get an empty rules dictionary, one can explicitly
399397
// set the `rules` key to `{}`.
400398
self.rules =
401-
try container.decodeIfPresent([String: Bool].self, forKey: .rules)
399+
try container.decodeIfPresent([String: Configuration.RuleSeverity].self, forKey: .rules)
402400
?? defaults.rules
403-
404-
self.ruleSeverity =
405-
try container.decodeIfPresent([String: RuleSeverity].self, forKey: .ruleSeverity) ?? [:]
406401
}
407402

408403
public func encode(to encoder: Encoder) throws {
@@ -499,3 +494,17 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable {
499494

500495
public init() {}
501496
}
497+
498+
499+
extension Configuration.RuleSeverity {
500+
func findingSeverity(ruleDefault: Finding.Severity) -> Finding.Severity {
501+
switch self {
502+
case .warning: return .warning
503+
case .error: return .error
504+
case .ruleDefault:
505+
return ruleDefault
506+
case .disabled:
507+
return .disabled
508+
}
509+
}
510+
}

‎Sources/SwiftFormat/API/Finding.swift

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public struct Finding {
1818
case error
1919
case refactoring
2020
case convention
21+
case disabled
2122
}
2223

2324
/// The file path and location in that file where a finding was encountered.

‎Sources/SwiftFormat/API/FindingCategorizing.swift

+1-11
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,9 @@ public protocol FindingCategorizing: CustomStringConvertible {
2121
///
2222
/// By default, all findings are warnings. Individual categories or configuration may choose to override this to
2323
/// make the findings in those categories more severe.
24-
func severity(configuration: Configuration) -> Finding.Severity
24+
var severity: Finding.Severity { get }
2525

2626
/// The name of the category.
2727
var name: String {get}
2828
}
2929

30-
extension FindingCategorizing {
31-
func severity(configuration: Configuration) -> Finding.Severity {
32-
return severityFromConfig(configuration: configuration)
33-
}
34-
35-
func severityFromConfig(configuration: Configuration) -> Finding.Severity {
36-
guard let customSeverity = configuration.ruleSeverity[self.name] else { return .warning }
37-
return customSeverity.findingSeverity
38-
}
39-
}

‎Sources/SwiftFormat/Core/Context.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ public final class Context {
108108
let ruleName = ruleNameCache[ObjectIdentifier(rule)] ?? R.ruleName
109109
switch ruleMask.ruleState(ruleName, at: loc) {
110110
case .default:
111-
return configuration.rules[ruleName] ?? false
111+
guard let configSeverity = configuration.rules[ruleName] else { return false }
112+
if case .disabled = configSeverity {
113+
return false
114+
} else {
115+
return true
116+
}
112117
case .disabled:
113118
return false
114119
}

‎Sources/SwiftFormat/Core/FindingEmitter.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ final class FindingEmitter {
5555
Finding(
5656
category: category,
5757
message: message,
58-
severity: category.severity(configuration: context.configuration),
58+
severity: category.severity,
5959
location: location,
6060
notes: notes
6161
)

‎Sources/SwiftFormat/Core/Rule.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ extension Rule {
8686
syntaxLocation = nil
8787
}
8888

89-
let severity: Finding.Severity? = severity ?? context.configuration.findingSeverity(for: type(of: self))
89+
let severity: Finding.Severity = severity ?? context.configuration.findingSeverity(for: type(of: self), defaultSeverity: .warning)
9090

9191
let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity)
9292
context.findingEmitter.emit(
@@ -100,8 +100,8 @@ extension Rule {
100100
}
101101

102102
extension Configuration {
103-
func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? {
104-
guard let severity = self.ruleSeverity[rule.ruleName] else { return nil }
105-
return severity.findingSeverity
103+
func findingSeverity(for rule: any Rule.Type, defaultSeverity: Finding.Severity) -> Finding.Severity {
104+
guard let severity = self.rules[rule.ruleName] else { return defaultSeverity }
105+
return severity.findingSeverity(ruleDefault: defaultSeverity)
106106
}
107107
}

‎Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift

+2-9
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,15 @@ struct RuleBasedFindingCategory: FindingCategorizing {
2222

2323
var description: String { ruleType.ruleName }
2424

25-
var severity: Finding.Severity?
25+
var severity: Finding.Severity
2626

2727
var name: String {
2828
return description
2929
}
3030

3131
/// Creates a finding category that wraps the given rule type.
32-
init(ruleType: Rule.Type, severity: Finding.Severity? = nil) {
32+
init(ruleType: Rule.Type, severity: Finding.Severity) {
3333
self.ruleType = ruleType
3434
self.severity = severity
3535
}
36-
37-
func severity(configuration: Configuration) -> Finding.Severity {
38-
if let severity = severity {
39-
return severity
40-
}
41-
return severityFromConfig(configuration: configuration)
42-
}
4336
}

‎Sources/SwiftFormat/Core/RuleRegistry+Generated.swift

+53-44
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,58 @@
1313
// This file is automatically generated with generate-swift-format. Do not edit!
1414

1515
@_spi(Internal) public enum RuleRegistry {
16-
public static let rules: [String: Bool] = [
17-
"AllPublicDeclarationsHaveDocumentation": false,
18-
"AlwaysUseLiteralForEmptyCollectionInit": false,
19-
"AlwaysUseLowerCamelCase": true,
20-
"AmbiguousTrailingClosureOverload": true,
21-
"AvoidRetroactiveConformances": true,
22-
"BeginDocumentationCommentWithOneLineSummary": false,
23-
"DoNotUseSemicolons": true,
24-
"DontRepeatTypeInStaticProperties": true,
25-
"FileScopedDeclarationPrivacy": true,
26-
"FullyIndirectEnum": true,
27-
"GroupNumericLiterals": true,
28-
"IdentifiersMustBeASCII": true,
29-
"NeverForceUnwrap": false,
30-
"NeverUseForceTry": false,
31-
"NeverUseImplicitlyUnwrappedOptionals": false,
32-
"NoAccessLevelOnExtensionDeclaration": true,
33-
"NoAssignmentInExpressions": true,
34-
"NoBlockComments": true,
35-
"NoCasesWithOnlyFallthrough": true,
36-
"NoEmptyLinesOpeningClosingBraces": false,
37-
"NoEmptyTrailingClosureParentheses": true,
38-
"NoLabelsInCasePatterns": true,
39-
"NoLeadingUnderscores": false,
40-
"NoParensAroundConditions": true,
41-
"NoPlaygroundLiterals": true,
42-
"NoVoidReturnOnFunctionSignature": true,
43-
"OmitExplicitReturns": false,
44-
"OneCasePerLine": true,
45-
"OneVariableDeclarationPerLine": true,
46-
"OnlyOneTrailingClosureArgument": true,
47-
"OrderedImports": true,
48-
"ReplaceForEachWithForLoop": true,
49-
"ReturnVoidInsteadOfEmptyTuple": true,
50-
"TypeNamesShouldBeCapitalized": true,
51-
"UseEarlyExits": false,
52-
"UseExplicitNilCheckInConditions": true,
53-
"UseLetInEveryBoundCaseVariable": true,
54-
"UseShorthandTypeNames": true,
55-
"UseSingleLinePropertyGetter": true,
56-
"UseSynthesizedInitializer": true,
57-
"UseTripleSlashForDocumentationComments": true,
58-
"UseWhereClausesInForLoops": false,
59-
"ValidateDocumentationComments": false,
16+
public static let rules: [String: Configuration.RuleSeverity] = [
17+
"AllPublicDeclarationsHaveDocumentation": .disabled,
18+
"AlwaysUseLiteralForEmptyCollectionInit": .disabled,
19+
"AlwaysUseLowerCamelCase": .ruleDefault,
20+
"AmbiguousTrailingClosureOverload": .ruleDefault,
21+
"AvoidRetroactiveConformances": .ruleDefault,
22+
"BeginDocumentationCommentWithOneLineSummary": .disabled,
23+
"DoNotUseSemicolons": .ruleDefault,
24+
"DontRepeatTypeInStaticProperties": .ruleDefault,
25+
"FileScopedDeclarationPrivacy": .ruleDefault,
26+
"FullyIndirectEnum": .ruleDefault,
27+
"GroupNumericLiterals": .ruleDefault,
28+
"IdentifiersMustBeASCII": .ruleDefault,
29+
"NeverForceUnwrap": .disabled,
30+
"NeverUseForceTry": .disabled,
31+
"NeverUseImplicitlyUnwrappedOptionals": .disabled,
32+
"NoAccessLevelOnExtensionDeclaration": .ruleDefault,
33+
"NoAssignmentInExpressions": .ruleDefault,
34+
"NoBlockComments": .ruleDefault,
35+
"NoCasesWithOnlyFallthrough": .ruleDefault,
36+
"NoEmptyLinesOpeningClosingBraces": .disabled,
37+
"NoEmptyTrailingClosureParentheses": .ruleDefault,
38+
"NoLabelsInCasePatterns": .ruleDefault,
39+
"NoLeadingUnderscores": .disabled,
40+
"NoParensAroundConditions": .ruleDefault,
41+
"NoPlaygroundLiterals": .ruleDefault,
42+
"NoVoidReturnOnFunctionSignature": .ruleDefault,
43+
"OmitExplicitReturns": .disabled,
44+
"OneCasePerLine": .ruleDefault,
45+
"OneVariableDeclarationPerLine": .ruleDefault,
46+
"OnlyOneTrailingClosureArgument": .ruleDefault,
47+
"OrderedImports": .ruleDefault,
48+
"ReplaceForEachWithForLoop": .ruleDefault,
49+
"ReturnVoidInsteadOfEmptyTuple": .ruleDefault,
50+
"TypeNamesShouldBeCapitalized": .ruleDefault,
51+
"UseEarlyExits": .disabled,
52+
"UseExplicitNilCheckInConditions": .ruleDefault,
53+
"UseLetInEveryBoundCaseVariable": .ruleDefault,
54+
"UseShorthandTypeNames": .ruleDefault,
55+
"UseSingleLinePropertyGetter": .ruleDefault,
56+
"UseSynthesizedInitializer": .ruleDefault,
57+
"UseTripleSlashForDocumentationComments": .ruleDefault,
58+
"UseWhereClausesInForLoops": .disabled,
59+
"ValidateDocumentationComments": .disabled,
60+
"AddLines": .ruleDefault,
61+
"EndOfLineComment": .ruleDefault,
62+
"Indentation": .ruleDefault,
63+
"LineLength": .ruleDefault,
64+
"RemoveLine": .ruleDefault,
65+
"Spacing": .ruleDefault,
66+
"SpacingCharacter": .ruleDefault,
67+
"TrailingComma": .ruleDefault,
68+
"TrailingWhitespace": .ruleDefault,
6069
]
6170
}

‎Sources/SwiftFormat/PrettyPrint/PrettyPrint.swift

+20-3
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ public class PrettyPrinter {
475475

476476
if wasEndOfLine {
477477
if !(canFit(comment.length) || isBreakingSuppressed) {
478-
diagnose(.moveEndOfLineComment, category: .endOfLineComment)
478+
diagnose(.moveEndOfLineComment, category: .endOfLineComment().withSeverity(configuration))
479479
}
480480
}
481481
outputBuffer.write(comment.print(indent: currentIndentation))
@@ -515,9 +515,9 @@ public class PrettyPrinter {
515515
startLineNumber != openCloseBreakCompensatingLineNumber && !isSingleElement
516516
&& configuration.multiElementCollectionTrailingCommas
517517
if shouldHaveTrailingComma && !hasTrailingComma {
518-
diagnose(.addTrailingComma, category: .trailingComma)
518+
diagnose(.addTrailingComma, category: .trailingComma().withSeverity(configuration))
519519
} else if !shouldHaveTrailingComma && hasTrailingComma {
520-
diagnose(.removeTrailingComma, category: .trailingComma)
520+
diagnose(.removeTrailingComma, category: .trailingComma().withSeverity(configuration))
521521
}
522522

523523
let shouldWriteComma = whitespaceOnly ? hasTrailingComma : shouldHaveTrailingComma
@@ -814,6 +814,7 @@ public class PrettyPrinter {
814814

815815
/// Emits a finding with the given message and category at the current location in `outputBuffer`.
816816
private func diagnose(_ message: Finding.Message, category: PrettyPrintFindingCategory) {
817+
if case .disabled = category.severity { return }
817818
// Add 1 since columns uses 1-based indices.
818819
let column = outputBuffer.column + 1
819820
context.findingEmitter.emit(
@@ -835,3 +836,19 @@ extension Finding.Message {
835836
fileprivate static let removeTrailingComma: Finding.Message =
836837
"remove trailing comma from the last element in single line collection literal"
837838
}
839+
840+
extension PrettyPrintFindingCategory {
841+
func withSeverity(_ configuration: Configuration) -> Self {
842+
let category: PrettyPrintFindingCategory = self
843+
let severity = configuration
844+
.rules[category.name]?
845+
.findingSeverity(ruleDefault: category.severity) ?? category.severity
846+
847+
switch self {
848+
case .endOfLineComment:
849+
return .endOfLineComment(severity)
850+
case .trailingComma:
851+
return .trailingComma(severity)
852+
}
853+
}
854+
}

‎Sources/SwiftFormat/PrettyPrint/PrettyPrintFindingCategory.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
enum PrettyPrintFindingCategory: FindingCategorizing {
1515

1616
/// Finding related to an end-of-line comment.
17-
case endOfLineComment
17+
case endOfLineComment(Finding.Severity = .warning)
1818

1919
/// Findings related to the presence of absence of a trailing comma in collection literals.
20-
case trailingComma
20+
case trailingComma(Finding.Severity = .warning)
2121

2222
var description: String {
2323
switch self {
@@ -30,4 +30,11 @@ enum PrettyPrintFindingCategory: FindingCategorizing {
3030
self.description
3131
}
3232

33+
var severity: Finding.Severity {
34+
switch self {
35+
case .endOfLineComment(let severity): return severity
36+
case .trailingComma(let severity): return severity
37+
}
38+
}
39+
3340
}

‎Sources/SwiftFormat/PrettyPrint/WhitespaceFindingCategory.swift

+19-7
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,25 @@
1313
/// Categories for findings emitted by the whitespace linter.
1414
enum WhitespaceFindingCategory: FindingCategorizing {
1515
/// Findings related to trailing whitespace on a line.
16-
case trailingWhitespace
16+
case trailingWhitespace(Finding.Severity = .warning)
1717

1818
/// Findings related to indentation (i.e., whitespace at the beginning of a line).
19-
case indentation
19+
case indentation(Finding.Severity = .warning)
2020

2121
/// Findings related to interior whitespace (i.e., neither leading nor trailing space).
22-
case spacing
22+
case spacing(Finding.Severity = .warning)
2323

2424
/// Findings related to specific characters used for interior whitespace.
25-
case spacingCharacter
25+
case spacingCharacter(Finding.Severity = .warning)
2626

2727
/// Findings related to the removal of line breaks.
28-
case removeLine
28+
case removeLine(Finding.Severity = .warning)
2929

3030
/// Findings related to the addition of line breaks.
31-
case addLines
31+
case addLines(Finding.Severity = .warning)
3232

3333
/// Findings related to the length of a line.
34-
case lineLength
34+
case lineLength(Finding.Severity = .warning)
3535

3636
var description: String {
3737
switch self {
@@ -48,4 +48,16 @@ enum WhitespaceFindingCategory: FindingCategorizing {
4848
var name: String {
4949
return self.description
5050
}
51+
52+
var severity: Finding.Severity {
53+
switch self {
54+
case .trailingWhitespace(let severity): return severity
55+
case .indentation(let severity): return severity
56+
case .spacing(let severity): return severity
57+
case .spacingCharacter(let severity): return severity
58+
case .removeLine(let severity): return severity
59+
case .addLines(let severity): return severity
60+
case .lineLength(let severity): return severity
61+
}
62+
}
5163
}

‎Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift

+34-7
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public class WhitespaceLinter {
131131
// If there were excess newlines in the user input, tell the user to remove them. This
132132
// short-circuits the trailing whitespace check below; we don't bother telling the user
133133
// about trailing whitespace on a line that we're also telling them to delete.
134-
diagnose(.removeLineError, category: .removeLine, utf8Offset: userIndex)
134+
diagnose(.removeLineError, category: .removeLine().withSeverity(context), utf8Offset: userIndex)
135135
userIndex += userRun.count + 1
136136
} else if runIndex != userRuns.count - 1 {
137137
if let formattedRun = possibleFormattedRun {
@@ -169,7 +169,7 @@ public class WhitespaceLinter {
169169
if excessFormattedLines > 0 && !isLineTooLong {
170170
diagnose(
171171
.addLinesError(excessFormattedLines),
172-
category: .addLines,
172+
category: .addLines().withSeverity(context),
173173
utf8Offset: userWhitespace.startIndex
174174
)
175175
}
@@ -249,7 +249,7 @@ public class WhitespaceLinter {
249249
}
250250

251251
isLineTooLong = true
252-
diagnose(.lineLengthError, category: .lineLength, utf8Offset: adjustedUserIndex)
252+
diagnose(.lineLengthError, category: .lineLength().withSeverity(context), utf8Offset: adjustedUserIndex)
253253
}
254254

255255
/// Compare user and formatted whitespace buffers, and check for indentation errors.
@@ -275,7 +275,7 @@ public class WhitespaceLinter {
275275
let expected = indentation(of: formattedRun)
276276
diagnose(
277277
.indentationError(expected: expected, actual: actual),
278-
category: .indentation,
278+
category: .indentation().withSeverity(context),
279279
utf8Offset: userIndex
280280
)
281281
}
@@ -292,7 +292,7 @@ public class WhitespaceLinter {
292292
formattedRun: ArraySlice<UTF8.CodeUnit>
293293
) {
294294
if userRun != formattedRun {
295-
diagnose(.trailingWhitespaceError, category: .trailingWhitespace, utf8Offset: userIndex)
295+
diagnose(.trailingWhitespaceError, category: .trailingWhitespace().withSeverity(context), utf8Offset: userIndex)
296296
}
297297
}
298298

@@ -316,10 +316,10 @@ public class WhitespaceLinter {
316316
// This assumes tabs will always be forbidden for inter-token spacing (but not for leading
317317
// indentation).
318318
if userRun.contains(utf8Tab) {
319-
diagnose(.spacingCharError, category: .spacingCharacter, utf8Offset: userIndex)
319+
diagnose(.spacingCharError, category: .spacingCharacter().withSeverity(context), utf8Offset: userIndex)
320320
} else if formattedRun.count != userRun.count {
321321
let delta = formattedRun.count - userRun.count
322-
diagnose(.spacingError(delta), category: .spacing, utf8Offset: userIndex)
322+
diagnose(.spacingError(delta), category: .spacing().withSeverity(context), utf8Offset: userIndex)
323323
}
324324
}
325325

@@ -375,6 +375,7 @@ public class WhitespaceLinter {
375375
category: WhitespaceFindingCategory,
376376
utf8Offset: Int
377377
) {
378+
if case .disabled = category.severity { return }
378379
let absolutePosition = AbsolutePosition(utf8Offset: utf8Offset)
379380
let sourceLocation = context.sourceLocationConverter.location(for: absolutePosition)
380381
context.findingEmitter.emit(
@@ -515,3 +516,29 @@ extension Finding.Message {
515516

516517
fileprivate static let lineLengthError: Finding.Message = "line is too long"
517518
}
519+
520+
extension WhitespaceFindingCategory {
521+
func withSeverity(_ context: Context) -> Self {
522+
let category: WhitespaceFindingCategory = self
523+
let severity = context.configuration
524+
.rules[category.name]?
525+
.findingSeverity(ruleDefault: category.severity) ?? category.severity
526+
527+
switch self {
528+
case .trailingWhitespace(_):
529+
return .trailingWhitespace(severity)
530+
case .indentation(_):
531+
return .indentation(severity)
532+
case .spacing(_):
533+
return .spacing(severity)
534+
case .spacingCharacter(_):
535+
return .spacingCharacter(severity)
536+
case .removeLine(_):
537+
return .removeLine(severity)
538+
case .addLines(_):
539+
return .addLines(severity)
540+
case .lineLength(_):
541+
return .lineLength(severity)
542+
}
543+
}
544+
}

‎Sources/_SwiftFormatTestSupport/Configuration+Testing.swift

+20
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,24 @@ extension Configuration {
4444
config.indentBlankLines = false
4545
return config
4646
}
47+
48+
public static func forTesting(enabledRule: String) -> Configuration {
49+
var config = Configuration.forTesting.disableAllRules()
50+
config.rules[enabledRule] = .ruleDefault
51+
return config
52+
}
53+
}
54+
55+
extension Configuration {
56+
public func disableAllRules() -> Self {
57+
var config = self
58+
config.rules = config.rules.mapValues({_ in .disabled})
59+
return config
60+
}
61+
62+
public func enable(_ rule: String, severity: Configuration.RuleSeverity) -> Self {
63+
var config = self
64+
config.rules[rule] = severity
65+
return config
66+
}
4767
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
@_spi(Rules) import SwiftFormat
15+
import SwiftParser
16+
import SwiftSyntax
17+
18+
/// Collects information about rules in the formatter code base.
19+
final class PrettyPrintCollector {
20+
21+
/// A list of all the format-only pretty-print categories found in the code base.
22+
var allPrettyPrinterCategories = Set<String>()
23+
24+
/// Populates the internal collections with rules in the given directory.
25+
///
26+
/// - Parameter url: The file system URL that should be scanned for rules.
27+
func collect(from url: URL) throws {
28+
// For each file in the Rules directory, find types that either conform to SyntaxLintRule or
29+
// inherit from SyntaxFormatRule.
30+
let fm = FileManager.default
31+
guard let rulesEnumerator = fm.enumerator(atPath: url.path) else {
32+
fatalError("Could not list the directory \(url.path)")
33+
}
34+
35+
for baseName in rulesEnumerator {
36+
// Ignore files that aren't Swift source files.
37+
guard let baseName = baseName as? String, baseName.hasSuffix(".swift") else { continue }
38+
39+
let fileURL = url.appendingPathComponent(baseName)
40+
let fileInput = try String(contentsOf: fileURL)
41+
let sourceFile = Parser.parse(source: fileInput)
42+
43+
for statement in sourceFile.statements {
44+
let pp = self.detectPrettyPrintCategories(at: statement)
45+
allPrettyPrinterCategories.formUnion(pp)
46+
}
47+
}
48+
}
49+
50+
private func detectPrettyPrintCategories(at statement: CodeBlockItemSyntax) -> [String] {
51+
guard let enumDecl = statement.item.as(EnumDeclSyntax.self) else {
52+
return []
53+
}
54+
55+
if enumDecl.name.text == "PrettyPrintFindingCategory" {
56+
print("HIT")
57+
}
58+
59+
// Make sure it has an inheritance clause.
60+
guard let inheritanceClause = enumDecl.inheritanceClause else {
61+
return []
62+
}
63+
64+
// Scan through the inheritance clause to find one of the protocols/types we're interested in.
65+
for inheritance in inheritanceClause.inheritedTypes {
66+
guard let identifier = inheritance.type.as(IdentifierTypeSyntax.self) else {
67+
continue
68+
}
69+
70+
if identifier.name.text != "FindingCategorizing" {
71+
// Keep looking at the other inheritances.
72+
continue
73+
}
74+
75+
// Now that we know it's a pretty printing category, collect the `description` method and extract the name.
76+
for member in enumDecl.memberBlock.members {
77+
guard let varDecl = member.decl.as(VariableDeclSyntax.self) else { continue }
78+
guard let descriptionDecl = varDecl.bindings
79+
.first(where: {
80+
$0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "description"
81+
}) else { continue }
82+
let pp = PrettyPrintCategoryVisitor(viewMode: .sourceAccurate)
83+
_ = pp.walk(descriptionDecl)
84+
return pp.prettyPrintCategories
85+
}
86+
}
87+
88+
return []
89+
}
90+
}
91+
92+
final class PrettyPrintCategoryVisitor: SyntaxVisitor {
93+
94+
var prettyPrintCategories: [String] = []
95+
96+
override func visit(_ node: StringSegmentSyntax) -> SyntaxVisitorContinueKind {
97+
prettyPrintCategories.append(node.content.text)
98+
return .skipChildren
99+
}
100+
}

‎Sources/generate-swift-format/RuleRegistryGenerator.swift

+19-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ final class RuleRegistryGenerator: FileGenerator {
1818
/// The rules collected by scanning the formatter source code.
1919
let ruleCollector: RuleCollector
2020

21+
/// The pretty-printing categories collected by scanning the formatter source code.
22+
let prettyPrintCollector: PrettyPrintCollector
23+
2124
/// Creates a new rule registry generator.
22-
init(ruleCollector: RuleCollector) {
25+
init(ruleCollector: RuleCollector, prettyPrintCollector: PrettyPrintCollector) {
2326
self.ruleCollector = ruleCollector
27+
self.prettyPrintCollector = prettyPrintCollector
2428
}
2529

2630
func write(into handle: FileHandle) throws {
@@ -41,14 +45,26 @@ final class RuleRegistryGenerator: FileGenerator {
4145
// This file is automatically generated with generate-swift-format. Do not edit!
4246
4347
@_spi(Internal) public enum RuleRegistry {
44-
public static let rules: [String: Bool] = [
48+
public static let rules: [String: Configuration.RuleSeverity] = [
4549
4650
"""
4751
)
4852

4953
for detectedRule in ruleCollector.allLinters.sorted(by: { $0.typeName < $1.typeName }) {
50-
handle.write(" \"\(detectedRule.typeName)\": \(!detectedRule.isOptIn),\n")
54+
handle.write(" \"\(detectedRule.typeName)\": \(severity(detectedRule.isOptIn)),\n")
55+
}
56+
57+
for ppCategory in prettyPrintCollector.allPrettyPrinterCategories.sorted(by: { $0 < $1 }) {
58+
handle.write(" \"\(ppCategory)\": .ruleDefault,\n")
5159
}
5260
handle.write(" ]\n}\n")
5361
}
62+
63+
func severity(_ isOptIn: Bool) -> String {
64+
if isOptIn {
65+
return ".disabled"
66+
} else {
67+
return ".ruleDefault"
68+
}
69+
}
5470
}

‎Sources/generate-swift-format/main.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ let rulesDirectory =
2020
sourcesDirectory
2121
.appendingPathComponent("SwiftFormat")
2222
.appendingPathComponent("Rules")
23+
let prettyPrintDirectory =
24+
sourcesDirectory
25+
.appendingPathComponent("SwiftFormat")
26+
.appendingPathComponent("PrettyPrint")
2327
let pipelineFile =
2428
sourcesDirectory
2529
.appendingPathComponent("SwiftFormat")
@@ -46,12 +50,15 @@ let ruleDocumentationFile =
4650
var ruleCollector = RuleCollector()
4751
try ruleCollector.collect(from: rulesDirectory)
4852

53+
var prettyPrintCollector = PrettyPrintCollector()
54+
try prettyPrintCollector.collect(from: prettyPrintDirectory)
55+
4956
// Generate a file with extensions for the lint and format pipelines.
5057
let pipelineGenerator = PipelineGenerator(ruleCollector: ruleCollector)
5158
try pipelineGenerator.generateFile(at: pipelineFile)
5259

5360
// Generate the rule registry dictionary for configuration.
54-
let registryGenerator = RuleRegistryGenerator(ruleCollector: ruleCollector)
61+
let registryGenerator = RuleRegistryGenerator(ruleCollector: ruleCollector, prettyPrintCollector: prettyPrintCollector)
5562
try registryGenerator.generateFile(at: ruleRegistryFile)
5663

5764
// Generate the rule name cache.

‎Sources/swift-format/Utilities/DiagnosticsEngine.swift

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ final class DiagnosticsEngine {
140140
case .warning: severity = .warning
141141
case .refactoring: severity = .warning
142142
case .convention: severity = .warning
143+
case .disabled: fatalError("must not be called for disabled findings")
143144
}
144145
return Diagnostic(
145146
severity: severity,

‎Tests/SwiftFormatTests/API/ConfigurationTests.swift

+3-6
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,10 @@ final class ConfigurationTests: XCTestCase {
1919
}
2020

2121
func testSeverityDecoding() {
22-
var config = Configuration()
23-
config.ruleSeverity["AlwaysUseLowerCamelCase"] = .warning
24-
config.ruleSeverity["AmbiguousTrailingClosureOverload"] = .error
25-
2622
let dictionaryData =
2723
"""
2824
{
29-
"ruleSeverity": {
25+
"rules": {
3026
"AlwaysUseLowerCamelCase": "warning",
3127
"AmbiguousTrailingClosureOverload": "error",
3228
}
@@ -36,7 +32,8 @@ final class ConfigurationTests: XCTestCase {
3632
let jsonConfig =
3733
try! jsonDecoder.decode(Configuration.self, from: dictionaryData)
3834

39-
XCTAssertEqual(config, jsonConfig)
35+
XCTAssertEqual(jsonConfig.rules["AlwaysUseLowerCamelCase"]!, .warning)
36+
XCTAssertEqual(jsonConfig.rules["AmbiguousTrailingClosureOverload"]!, .error)
4037
}
4138

4239
func testMissingConfigurationFile() throws {

‎Tests/SwiftFormatTests/PrettyPrint/WhitespaceTestCase.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ class WhitespaceTestCase: DiagnosingTestCase {
2121
input: String,
2222
expected: String,
2323
linelength: Int? = nil,
24+
configuration: Configuration = Configuration.forTesting,
2425
findings: [FindingSpec],
2526
file: StaticString = #file,
2627
line: UInt = #line
2728
) {
2829
let markedText = MarkedText(textWithMarkers: input)
2930

3031
let sourceFileSyntax = Parser.parse(source: markedText.textWithoutMarkers)
31-
var configuration = Configuration.forTesting
32+
var configuration = configuration
3233
if let linelength = linelength {
3334
configuration.lineLength = linelength
3435
}

‎Tests/SwiftFormatTests/Rules/FileScopedDeclarationPrivacyTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ final class FileScopedDeclarationPrivacyTests: LintOrFormatRuleTestCase {
167167
findingsProvider: (String, String) -> [FindingSpec]
168168
) {
169169
for testConfig in testConfigurations {
170-
var configuration = Configuration.forTesting
170+
var configuration = Configuration.forTesting(enabledRule: FileScopedDeclarationPrivacy.self.ruleName)
171171
configuration.fileScopedDeclarationPrivacy.accessLevel = testConfig.desired
172172

173173
let substitutedInput = source.replacingOccurrences(of: "$access$", with: testConfig.original)

‎Tests/SwiftFormatTests/Rules/LintOrFormatRuleTestCase.swift

+3-8
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
3737
var emittedFindings = [Finding]()
3838

3939
// Force the rule to be enabled while we test it.
40-
var configuration = Configuration.forTesting
41-
configuration.rules[type.ruleName] = true
40+
let configuration = Configuration.forTesting(enabledRule: type.ruleName)
4241
let context = makeContext(
4342
sourceFileSyntax: sourceFileSyntax,
4443
configuration: configuration,
@@ -47,8 +46,6 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
4746
)
4847

4948
var emittedPipelineFindings = [Finding]()
50-
// Disable default rules, so only select rule runs in pipeline
51-
configuration.rules = [type.ruleName: true]
5249
let pipeline = SwiftLinter(
5350
configuration: configuration,
5451
findingConsumer: { emittedPipelineFindings.append($0) }
@@ -106,8 +103,8 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
106103
var emittedFindings = [Finding]()
107104

108105
// Force the rule to be enabled while we test it.
109-
var configuration = configuration ?? Configuration.forTesting
110-
configuration.rules[formatType.ruleName] = true
106+
let configuration = configuration ?? Configuration.forTesting(enabledRule: formatType.ruleName)
107+
111108
let context = makeContext(
112109
sourceFileSyntax: sourceFileSyntax,
113110
configuration: configuration,
@@ -150,8 +147,6 @@ class LintOrFormatRuleTestCase: DiagnosingTestCase {
150147
)
151148

152149
var emittedPipelineFindings = [Finding]()
153-
// Disable default rules, so only select rule runs in pipeline
154-
configuration.rules = [formatType.ruleName: true]
155150
let pipeline = SwiftFormatter(
156151
configuration: configuration,
157152
findingConsumer: { emittedPipelineFindings.append($0) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
@_spi(Rules) import SwiftFormat
2+
import _SwiftFormatTestSupport
3+
4+
final class SeverityOverrideRuleTest: LintOrFormatRuleTestCase {
5+
func testDoNotUseSemicolonAsError() {
6+
7+
var config = Configuration.forTesting.disableAllRules()
8+
config.rules[DoNotUseSemicolons.self.ruleName] = .error
9+
10+
assertFormatting(
11+
DoNotUseSemicolons.self,
12+
input: """
13+
print("hello")1️⃣;
14+
""",
15+
expected: """
16+
print("hello")
17+
""",
18+
findings: [
19+
FindingSpec("1️⃣", message: "remove ';'", severity: .error),
20+
],
21+
configuration: config
22+
)
23+
}
24+
25+
func testDoNotUseSemicolonDisabled() {
26+
27+
var config = Configuration.forTesting.disableAllRules()
28+
config.rules[DoNotUseSemicolons.self.ruleName] = .disabled
29+
30+
assertFormatting(
31+
DoNotUseSemicolons.self,
32+
input: """
33+
print("hello");
34+
""",
35+
expected: """
36+
print("hello");
37+
""",
38+
findings: [],
39+
configuration: config
40+
)
41+
}
42+
}
43+
44+
final class SeverityOverridePrettyPrintTest: PrettyPrintTestCase {
45+
46+
func testTrailingCommaDiagnosticsDisabled() {
47+
assertPrettyPrintEqual(
48+
input: """
49+
let a = [1, 2, 3,]
50+
""",
51+
expected: """
52+
let a = [1, 2, 3,]
53+
54+
""",
55+
linelength: 45,
56+
configuration: Configuration.forTesting.disableAllRules().enable("TrailingComma", severity: .disabled),
57+
whitespaceOnly: true,
58+
findings: []
59+
)
60+
}
61+
62+
func testTrailingCommaDiagnosticsAsError() {
63+
assertPrettyPrintEqual(
64+
input: """
65+
let a = [1, 2, 31️⃣,]
66+
""",
67+
expected: """
68+
let a = [1, 2, 3,]
69+
70+
""",
71+
linelength: 45,
72+
configuration: Configuration.forTesting.disableAllRules().enable("TrailingComma", severity: .error),
73+
whitespaceOnly: true,
74+
findings: [
75+
FindingSpec("1️⃣", message: "remove trailing comma from the last element in single line collection literal", severity: .error),
76+
]
77+
)
78+
}
79+
}
80+
81+
final class SeverityOverrideWhitespaceTest: WhitespaceTestCase {
82+
func testTrailingWhitespaceAsError() {
83+
assertWhitespaceLint(
84+
input: """
85+
let a = 1231️⃣\u{20}\u{20}
86+
87+
""",
88+
expected: """
89+
let a = 123
90+
91+
""",
92+
configuration: Configuration.forTesting.disableAllRules().enable("TrailingWhitespace", severity: .error),
93+
findings: [
94+
FindingSpec("1️⃣", message: "remove trailing whitespace", severity: .error),
95+
]
96+
)
97+
}
98+
99+
func testTrailingWhitespaceDisabled() {
100+
assertWhitespaceLint(
101+
input: """
102+
let a = 123\u{20}\u{20}
103+
104+
""",
105+
expected: """
106+
let a = 123
107+
108+
""",
109+
configuration: Configuration.forTesting.disableAllRules().enable("TrailingWhitespace", severity: .disabled),
110+
findings: []
111+
)
112+
}
113+
}

0 commit comments

Comments
 (0)
Please sign in to comment.