Skip to content

Commit 13fa278

Browse files
committed
Allow to customize Rule severity
In order to customize the severity of rules, I added the possibility to do so via the configuration files. If no severity is specified, we use the one pre-determined by the Rule itself. Example: ``` { "ruleSeverity": { "AlwaysUseLowerCamelCase": "warning", "AmbiguousTrailingClosureOverload": "error", } } ``` Issue: #879
1 parent c7a8b75 commit 13fa278

9 files changed

+94
-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
@@ -40,6 +45,7 @@ public struct Configuration: Codable, Equatable {
4045
case fileScopedDeclarationPrivacy
4146
case indentSwitchCaseLabels
4247
case rules
48+
case ruleSeverity
4349
case spacesAroundRangeFormationOperators
4450
case noAssignmentInExpressions
4551
case multiElementCollectionTrailingCommas
@@ -60,6 +66,10 @@ public struct Configuration: Codable, Equatable {
6066
/// marked as `false`, or if it is missing from the dictionary.
6167
public var rules: [String: Bool]
6268

69+
/// The dictionary containing the severities for the rule names that we wish to run on. If a rule
70+
/// is not listed here, the default severity is used.
71+
public var ruleSeverity: [String: RuleSeverity]
72+
6373
/// The maximum number of consecutive blank lines that may appear in a file.
6474
public var maximumBlankLines: Int
6575

@@ -280,6 +290,9 @@ public struct Configuration: Codable, Equatable {
280290
self.rules =
281291
try container.decodeIfPresent([String: Bool].self, forKey: .rules)
282292
?? defaults.rules
293+
294+
self.ruleSeverity =
295+
try container.decodeIfPresent([String: RuleSeverity].self, forKey: .ruleSeverity) ?? [:]
283296
}
284297

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

Sources/SwiftFormat/Core/Rule.swift

+12
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ extension Rule {
8282
syntaxLocation = nil
8383
}
8484

85+
let severity: Finding.Severity? = severity ?? context.configuration.findingSeverity(for: type(of: self))
86+
8587
let category = RuleBasedFindingCategory(ruleType: type(of: self), severity: severity)
8688
context.findingEmitter.emit(
8789
message,
@@ -90,3 +92,13 @@ extension Rule {
9092
notes: notes)
9193
}
9294
}
95+
96+
extension Configuration {
97+
func findingSeverity(for rule: any Rule.Type) -> Finding.Severity? {
98+
guard let severity = self.ruleSeverity[rule.ruleName] else { return nil }
99+
switch severity {
100+
case .warning: return .warning
101+
case .error: return .error
102+
}
103+
}
104+
}

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
@@ -139,6 +139,18 @@ open class DiagnosingTestCase: XCTestCase {
139139
file: file,
140140
line: line)
141141
}
142+
143+
XCTAssertEqual(
144+
matchedFinding.severity,
145+
findingSpec.severity,
146+
"""
147+
Finding emitted at marker '\(findingSpec.marker)' \
148+
(line:col \(markerLocation.line):\(markerLocation.column), offset \(utf8Offset)) \
149+
had the wrong severity
150+
""",
151+
file: file,
152+
line: line
153+
)
142154
}
143155

144156
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() {
2243
#if os(Windows)
2344
let path = #"C:\test.swift"#

Tests/SwiftFormatTests/Rules/OmitReturnsTests.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
1717
}
1818
""",
1919
findings: [
20-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression")
20+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring)
2121
])
2222
}
2323

@@ -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

@@ -75,8 +75,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
7575
}
7676
""",
7777
findings: [
78-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"),
79-
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression")
78+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
79+
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
8080
])
8181
}
8282

@@ -112,8 +112,8 @@ final class OmitReturnsTests: LintOrFormatRuleTestCase {
112112
}
113113
""",
114114
findings: [
115-
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression"),
116-
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression")
115+
FindingSpec("1️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
116+
FindingSpec("2️⃣", message: "'return' can be omitted because body consists of a single expression", severity: .refactoring),
117117
])
118118
}
119119
}

Tests/SwiftFormatTests/Rules/TypeNamesShouldBeCapitalizedTests.swift

+18-18
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
1818
}
1919
""",
2020
findings: [
21-
FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'"),
22-
FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'"),
23-
FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'"),
24-
FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'"),
25-
FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'"),
21+
FindingSpec("1️⃣", message: "rename the struct 'a' using UpperCamelCase; for example, 'A'", severity: .convention),
22+
FindingSpec("2️⃣", message: "rename the class 'klassName' using UpperCamelCase; for example, 'KlassName'", severity: .convention),
23+
FindingSpec("3️⃣", message: "rename the struct 'subType' using UpperCamelCase; for example, 'SubType'", severity: .convention),
24+
FindingSpec("4️⃣", message: "rename the protocol 'myProtocol' using UpperCamelCase; for example, 'MyProtocol'", severity: .convention),
25+
FindingSpec("5️⃣", message: "rename the struct 'innerType' using UpperCamelCase; for example, 'InnerType'", severity: .convention),
2626
]
2727
)
2828
}
@@ -37,8 +37,8 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
3737
distributed actor DistGreeter {}
3838
""",
3939
findings: [
40-
FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'"),
41-
FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'"),
40+
FindingSpec("1️⃣", message: "rename the actor 'myActor' using UpperCamelCase; for example, 'MyActor'", severity: .convention),
41+
FindingSpec("2️⃣", message: "rename the actor 'greeter' using UpperCamelCase; for example, 'Greeter'", severity: .convention),
4242
]
4343
)
4444
}
@@ -64,9 +64,9 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
6464
}
6565
""",
6666
findings: [
67-
FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'"),
68-
FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'"),
69-
FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'"),
67+
FindingSpec("1️⃣", message: "rename the associated type 'kind' using UpperCamelCase; for example, 'Kind'", severity: .convention),
68+
FindingSpec("2️⃣", message: "rename the type alias 'x' using UpperCamelCase; for example, 'X'", severity: .convention),
69+
FindingSpec("3️⃣", message: "rename the type alias 'data' using UpperCamelCase; for example, 'Data'", severity: .convention),
7070
]
7171
)
7272
}
@@ -108,14 +108,14 @@ final class TypeNamesShouldBeCapitalizedTests: LintOrFormatRuleTestCase {
108108
distributed actor __InternalGreeter {}
109109
""",
110110
findings: [
111-
FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'"),
112-
FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'"),
113-
FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'"),
114-
FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'"),
115-
FindingSpec("5️⃣", message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'"),
116-
FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'"),
117-
FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'"),
118-
FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'"),
111+
FindingSpec("1️⃣", message: "rename the protocol '_p' using UpperCamelCase; for example, '_P'", severity: .convention),
112+
FindingSpec("2️⃣", message: "rename the associated type '_value' using UpperCamelCase; for example, '_Value'", severity: .convention),
113+
FindingSpec("3️⃣", message: "rename the struct '_data' using UpperCamelCase; for example, '_Data'", severity: .convention),
114+
FindingSpec("4️⃣", message: "rename the type alias '_x' using UpperCamelCase; for example, '_X'", severity: .convention),
115+
FindingSpec("5️⃣", message: "rename the actor '_internalActor' using UpperCamelCase; for example, '_InternalActor'", severity: .convention),
116+
FindingSpec("6️⃣", message: "rename the enum '__e' using UpperCamelCase; for example, '__E'", severity: .convention),
117+
FindingSpec("7️⃣", message: "rename the class '_myClass' using UpperCamelCase; for example, '_MyClass'", severity: .convention),
118+
FindingSpec("8️⃣", message: "rename the actor '__greeter' using UpperCamelCase; for example, '__Greeter'", severity: .convention),
119119
]
120120
)
121121
}

0 commit comments

Comments
 (0)