Skip to content

Commit 22557b7

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: ``` { "ruleSeverity": { "AlwaysUseLowerCamelCase": "warning", "AmbiguousTrailingClosureOverload": "error", } } ``` Issue: swiftlang#879
1 parent f9b10f2 commit 22557b7

9 files changed

+95
-25
lines changed
 

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

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

‎Sources/SwiftFormat/API/Configuration.swift

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ internal let highestSupportedConfigurationVersion = 1
2424
/// Holds the complete set of configured values and defaults.
2525
public struct Configuration: Codable, Equatable {
2626

27+
public enum RuleSeverity: String, Codable, CaseIterable, Equatable, Sendable {
28+
case warning = "warning"
29+
case error = "error"
30+
}
31+
2732
private enum CodingKeys: CodingKey {
2833
case version
2934
case maximumBlankLines
@@ -42,6 +47,7 @@ public struct Configuration: Codable, Equatable {
4247
case fileScopedDeclarationPrivacy
4348
case indentSwitchCaseLabels
4449
case rules
50+
case ruleSeverity
4551
case spacesAroundRangeFormationOperators
4652
case noAssignmentInExpressions
4753
case multiElementCollectionTrailingCommas
@@ -64,6 +70,10 @@ public struct Configuration: Codable, Equatable {
6470
/// marked as `false`, or if it is missing from the dictionary.
6571
public var rules: [String: Bool]
6672

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]
76+
6777
/// The maximum number of consecutive blank lines that may appear in a file.
6878
public var maximumBlankLines: Int
6979

@@ -390,6 +400,9 @@ public struct Configuration: Codable, Equatable {
390400
self.rules =
391401
try container.decodeIfPresent([String: Bool].self, forKey: .rules)
392402
?? defaults.rules
403+
404+
self.ruleSeverity =
405+
try container.decodeIfPresent([String: RuleSeverity].self, forKey: .ruleSeverity) ?? [:]
393406
}
394407

395408
public func encode(to encoder: Encoder) throws {

‎Sources/SwiftFormat/Core/Rule.swift

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

89+
let severity: Finding.Severity? = severity ?? context.configuration.findingSeverity(for: type(of: self))
90+
8991
let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity)
9092
context.findingEmitter.emit(
9193
message,
@@ -95,3 +97,13 @@ extension Rule {
9597
)
9698
}
9799
}
100+
101+
extension Configuration {
102+
func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? {
103+
guard let severity = self.ruleSeverity[rule.ruleName] else { return nil }
104+
switch severity {
105+
case .warning: return .warning
106+
case .error: return .error
107+
}
108+
}
109+
}

‎Sources/SwiftFormat/Core/RuleBasedFindingCategory.swift

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ struct RuleBasedFindingCategory: FindingCategorizing {
2424

2525
var severity: Finding.Severity?
2626

27+
public var defaultSeverity: Finding.Severity {
28+
return severity ?? .warning
29+
}
30+
2731
/// Creates a finding category that wraps the given rule type.
2832
init(ruleType: Rule.Type, severity: Finding.Severity? = nil) {
2933
self.ruleType = ruleType

‎Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift

+12
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ open class DiagnosingTestCase: XCTestCase {
145145
line: line
146146
)
147147
}
148+
149+
XCTAssertEqual(
150+
matchedFinding.severity,
151+
findingSpec.severity,
152+
"""
153+
Finding emitted at marker '\(findingSpec.marker)' \
154+
(line:col \(markerLocation.line):\(markerLocation.column), offset \(utf8Offset)) \
155+
had the wrong severity
156+
""",
157+
file: file,
158+
line: line
159+
)
148160
}
149161

150162
private func assertAndRemoveNote(

‎Sources/_SwiftFormatTestSupport/FindingSpec.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftFormat
14+
1315
/// A description of a `Finding` that can be asserted during tests.
1416
public struct FindingSpec {
1517
/// The marker that identifies the finding.
@@ -21,11 +23,15 @@ public struct FindingSpec {
2123
/// A description of a `Note` that should be associated with this finding.
2224
public var notes: [NoteSpec]
2325

26+
/// A description of a `Note` that should be associated with this finding.
27+
public var severity: Finding.Severity
28+
2429
/// Creates a new `FindingSpec` with the given values.
25-
public init(_ marker: String = "1️⃣", message: String, notes: [NoteSpec] = []) {
30+
public init(_ marker: String = "1️⃣", message: String, notes: [NoteSpec] = [], severity: Finding.Severity = .warning) {
2631
self.marker = marker
2732
self.message = message
2833
self.notes = notes
34+
self.severity = severity
2935
}
3036
}
3137

‎Tests/SwiftFormatTests/API/ConfigurationTests.swift

+21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ final class ConfigurationTests: XCTestCase {
1818
XCTAssertEqual(defaultInitConfig, emptyJSONConfig)
1919
}
2020

21+
func testSeverityDecoding() {
22+
var config = Configuration()
23+
config.ruleSeverity["AlwaysUseLowerCamelCase"] = .warning
24+
config.ruleSeverity["AmbiguousTrailingClosureOverload"] = .error
25+
26+
let dictionaryData =
27+
"""
28+
{
29+
"ruleSeverity": {
30+
"AlwaysUseLowerCamelCase": "warning",
31+
"AmbiguousTrailingClosureOverload": "error",
32+
}
33+
}
34+
""".data(using: .utf8)!
35+
let jsonDecoder = JSONDecoder()
36+
let jsonConfig =
37+
try! jsonDecoder.decode(Configuration.self, from: dictionaryData)
38+
39+
XCTAssertEqual(config, jsonConfig)
40+
}
41+
2142
func testMissingConfigurationFile() throws {
2243
#if os(Windows)
2344
#if compiler(<6.0.2)

‎Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
1616
}
1717
""",
1818
findings: [
19-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression")
19+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring)
2020
]
2121
)
2222
}
@@ -35,7 +35,7 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
3535
}
3636
""",
3737
findings: [
38-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression")
38+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring)
3939
]
4040
)
4141
}
@@ -76,8 +76,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
7676
}
7777
""",
7878
findings: [
79-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"),
80-
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression"),
79+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
80+
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
8181
]
8282
)
8383
}
@@ -114,8 +114,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
114114
}
115115
""",
116116
findings: [
117-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"),
118-
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression"),
117+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
118+
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
119119
]
120120
)
121121
}

‎Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift

+19-18
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
1717
}
1818
""",
1919
findings: [
20-
FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'"),
21-
FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'"),
22-
FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'"),
23-
FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'"),
24-
FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'"),
20+
FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'", severity: .convention),
21+
FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'", severity: .convention),
22+
FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'", severity: .convention),
23+
FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'", severity: .convention),
24+
FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'", severity: .convention),
2525
]
2626
)
2727
}
@@ -36,8 +36,8 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
3636
distributed actor DistGreeter {}
3737
""",
3838
findings: [
39-
FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'"),
40-
FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'"),
39+
FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'", severity: .convention),
40+
FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'", severity: .convention),
4141
]
4242
)
4343
}
@@ -63,9 +63,9 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
6363
}
6464
""",
6565
findings: [
66-
FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'"),
67-
FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'"),
68-
FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'"),
66+
FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'", severity: .convention),
67+
FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'", severity: .convention),
68+
FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'", severity: .convention),
6969
]
7070
)
7171
}
@@ -107,17 +107,18 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
107107
distributed actor __InternalGreeter {}
108108
""",
109109
findings: [
110-
FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'"),
111-
FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'"),
112-
FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'"),
113-
FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'"),
110+
FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'", severity: .convention),
111+
FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'", severity: .convention),
112+
FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'", severity: .convention),
113+
FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'", severity: .convention),
114114
FindingSpec(
115115
"5️⃣",
116-
message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'"
116+
message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'",
117+
severity: .convention
117118
),
118-
FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'"),
119-
FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'"),
120-
FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'"),
119+
FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'", severity: .convention),
120+
FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'", severity: .convention),
121+
FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'", severity: .convention),
121122
]
122123
)
123124
}

0 commit comments

Comments
 (0)
Please sign in to comment.