Skip to content

Commit ac0b0d1

Browse files
committed
[SwiftWarningControl] Add ability to query warning group behavior including client-specified enclosing control rules
Adds an optional parameter `with enclosingControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] = [:]` to both: - `SyntaxProtocol.warningGroupControlRegionTree` - `SyntaxProtocol.warningGroupBehavior` to specify enclosing global warning group behavior controls which span the entire syntax node. This mechanism will be used by the compiler to specify global controls over certain diagnostic groups from the compilation configuration. For example, a command-line-specified `-Werror Group` will become an input to this mechanism to be used as *the* behavior control at source positions where no syntactic control (`@warn`) is specified. With this in place, the `SwiftWarningControl` API can be used as a single query which captures both syntactic and command-line configuration of warning diagnostic group behavior.
1 parent bd53a85 commit ac0b0d1

File tree

8 files changed

+114
-52
lines changed

8 files changed

+114
-52
lines changed

Sources/SwiftWarningControl/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

99
add_swift_syntax_library(SwiftWarningControl
10-
WarningGroupBehavior.swift
10+
WarningGroupControl.swift
1111
WarningControlDeclSyntax.swift
1212
WarningControlRegionBuilder.swift
1313
WarningControlRegions.swift

Sources/SwiftWarningControl/SwiftWarningControl.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ func foo() {
2525

2626
The `SwiftWarningControl` library provides a utility to determine, for a given source location and diagnostic group identifier, whether or not its behavior is affected by an `@warn` attribute of any of its parent declaration scope.
2727

28-
* `SyntaxProtocol.getWarningGroupControl(for diagnosticGroupIdentifier:)` produces the behavior specifier (`WarningGroupBehavior`: `error`, `warning`, `ignored`) which applies at this node.
28+
* `SyntaxProtocol.getWarningGroupControl(for diagnosticGroupIdentifier:)` produces the behavior control specifier (`WarningGroupControl`: `error`, `warning`, `ignored`) which applies at this node.
2929

30-
* `SyntaxProtocol.warningGroupControlRegionTree` holds a computed `WarningControlRegionTree` data structure value that can be used to efficiently test for the specified `WarningGroupBehavior` at a given source location and a given diagnostic group.
30+
* `SyntaxProtocol.warningGroupControlRegionTree` holds a computed `WarningControlRegionTree` data structure value that can be used to efficiently test for the specified `WarningGroupControl` at a given source location and a given diagnostic group.

Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import SwiftDiagnostics
1414
import SwiftSyntax
1515

1616
extension SyntaxProtocol {
17-
/// Get the warning emission behavior for the specified diagnostic group
17+
/// Get the warning emission behavior control for the specified diagnostic group
1818
/// by determining its containing `WarningControlRegion`, if one is present.
1919
@_spi(ExperimentalLanguageFeatures)
20-
public func warningGroupBehavior(
21-
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier
22-
) -> WarningGroupBehavior? {
23-
let warningControlRegions = root.warningGroupControlRegionTreeImpl(containing: self.position)
24-
return warningControlRegions.warningGroupBehavior(at: self.position, for: diagnosticGroupIdentifier)
20+
public func warningGroupControl(
21+
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
22+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
23+
) -> WarningGroupControl? {
24+
let warningControlRegions = root.warningGroupControlRegionTreeImpl(
25+
globalControls: globalControls,
26+
containing: self.position
27+
)
28+
return warningControlRegions.warningGroupControl(at: self.position, for: diagnosticGroupIdentifier)
2529
}
2630
}

Sources/SwiftWarningControl/WarningControlDeclSyntax.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
import SwiftSyntax
1414

1515
extension WithAttributesSyntax {
16-
/// Compute a dictionary of all `@warn` diagnostic group behaviors
16+
/// Compute a dictionary of all `@warn` diagnostic group behavior controls
1717
/// specified on this warning control declaration scope.
18-
var allWarningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] {
19-
attributes.reduce(into: [DiagnosticGroupIdentifier: WarningGroupBehavior]()) { result, attr in
18+
var allWarningGroupControls: [DiagnosticGroupIdentifier: WarningGroupControl] {
19+
attributes.reduce(into: [DiagnosticGroupIdentifier: WarningGroupControl]()) { result, attr in
2020
// `@warn` attributes
2121
guard case .attribute(let attributeSyntax) = attr,
2222
attributeSyntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "warn"
@@ -33,7 +33,7 @@ extension WithAttributesSyntax {
3333
return
3434
}
3535

36-
// Second argument is the `as: ` behavior specifier
36+
// Second argument is the `as: ` behavior control specifier
3737
guard
3838
let asParamExprSyntax = attributeSyntax
3939
.arguments?.as(LabeledExprListSyntax.self)?
@@ -43,14 +43,14 @@ extension WithAttributesSyntax {
4343
}
4444
guard
4545
asParamExprSyntax.label?.text == "as",
46-
let behaviorText = asParamExprSyntax
46+
let controlText = asParamExprSyntax
4747
.expression.as(DeclReferenceExprSyntax.self)?
4848
.baseName.text,
49-
let behavior = WarningGroupBehavior(rawValue: behaviorText)
49+
let control = WarningGroupControl(rawValue: controlText)
5050
else {
5151
return
5252
}
53-
result[DiagnosticGroupIdentifier(diagnosticGroupID)] = behavior
53+
result[DiagnosticGroupIdentifier(diagnosticGroupID)] = control
5454
}
5555
}
5656
}

Sources/SwiftWarningControl/WarningControlRegionBuilder.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,22 @@ import SwiftSyntax
1515
/// Compute the full set of warning control regions in this syntax node
1616
extension SyntaxProtocol {
1717
@_spi(ExperimentalLanguageFeatures)
18-
public func warningGroupControlRegionTree() -> WarningControlRegionTree {
19-
return warningGroupControlRegionTreeImpl()
18+
public func warningGroupControlRegionTree(
19+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
20+
) -> WarningControlRegionTree {
21+
return warningGroupControlRegionTreeImpl(globalControls: globalControls)
2022
}
2123

2224
/// Implementation of constructing a region tree with an optional parameter
2325
/// to specify that the constructed tree must only contain nodes which contain
2426
/// a specific absolute position - meant to speed up tree generation for individual
2527
/// queries.
26-
func warningGroupControlRegionTreeImpl(containing position: AbsolutePosition? = nil) -> WarningControlRegionTree {
28+
func warningGroupControlRegionTreeImpl(
29+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl],
30+
containing position: AbsolutePosition? = nil
31+
) -> WarningControlRegionTree {
2732
let visitor = WarningControlRegionVisitor(self.range, containing: position)
33+
visitor.tree.addWarningGroupControls(range: self.range, controls: globalControls)
2834
visitor.walk(self)
2935
return visitor.tree
3036
}

Sources/SwiftWarningControl/WarningControlRegions.swift

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,21 @@
1313
import SwiftSyntax
1414

1515
/// A single warning control region, consisting of a start and end positions,
16-
/// a diagnostic group identifier, and an emission behavior specifier.
16+
/// a diagnostic group identifier, and an emission behavior control specifier.
1717
@_spi(ExperimentalLanguageFeatures)
1818
public struct WarningControlRegion {
1919
public let range: Range<AbsolutePosition>
2020
public let diagnosticGroupIdentifier: DiagnosticGroupIdentifier
21-
public let behavior: WarningGroupBehavior
21+
public let control: WarningGroupControl
2222

2323
init(
2424
range: Range<AbsolutePosition>,
2525
diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
26-
behavior: WarningGroupBehavior
26+
control: WarningGroupControl
2727
) {
2828
self.range = range
2929
self.diagnosticGroupIdentifier = diagnosticGroupIdentifier
30-
self.behavior = behavior
30+
self.control = control
3131
}
3232
}
3333

@@ -98,9 +98,6 @@ public struct DiagnosticGroupIdentifier: Hashable, Sendable, ExpressibleByString
9898
/// traversal until we find the first containing region which specifies warning
9999
/// behavior control for the given diagnostic group id.
100100
///
101-
/// TODO: Capture global configuration from command-line arguments
102-
/// to represent global rules, such as `-Werror`, `-Wwarning`,
103-
/// and `-suppress-warnings` as *the* root region node.
104101
@_spi(ExperimentalLanguageFeatures)
105102
public struct WarningControlRegionTree {
106103
/// Root region representing top-level (file) scope
@@ -113,11 +110,12 @@ public struct WarningControlRegionTree {
113110
/// Add a warning control region to the tree
114111
mutating func addWarningGroupControls(
115112
range: Range<AbsolutePosition>,
116-
controls: [DiagnosticGroupIdentifier: WarningGroupBehavior]
113+
controls: [DiagnosticGroupIdentifier: WarningGroupControl]
117114
) {
115+
guard !controls.isEmpty else { return }
118116
let newNode = WarningControlRegionNode(range: range)
119-
for (diagnosticGroupIdentifier, behavior) in controls {
120-
newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, behavior: behavior)
117+
for (diagnosticGroupIdentifier, control) in controls {
118+
newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control)
121119
}
122120
insertIntoSubtree(newNode, parent: rootRegionNode)
123121
}
@@ -134,7 +132,7 @@ public struct WarningControlRegionTree {
134132
// Check if the new region has the same boundaries as the parent
135133
if parent.range == node.range {
136134
for (diagnosticGroupIdentifier, control) in node.warningGroupControls {
137-
parent.addWarningGroupControl(for: diagnosticGroupIdentifier, behavior: control)
135+
parent.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control)
138136
}
139137
return
140138
}
@@ -157,7 +155,8 @@ extension WarningControlRegionTree: CustomDebugStringConvertible {
157155
let spacing = String(repeating: " ", count: indent)
158156
result += "\(spacing)[\(node.range.lowerBound), \(node.range.upperBound)]"
159157
if !node.warningGroupControls.isEmpty {
160-
result += " id(s): \(node.warningGroupControls.keys.map { $0.identifier }.joined(separator: ", "))\n"
158+
result +=
159+
" control(s): \(node.warningGroupControls.map { $0.key.identifier + ": " + $0.value.rawValue }.joined(separator: ", "))\n"
161160
} else {
162161
result += "\n"
163162
}
@@ -172,31 +171,31 @@ extension WarningControlRegionTree: CustomDebugStringConvertible {
172171
}
173172

174173
extension WarningControlRegionTree {
175-
/// Determine the warning group behavior at a specified position
176-
/// for a given diagnostic group
174+
/// Determine the warning group behavior control at a specified position
175+
/// for a given diagnostic group.
177176
@_spi(ExperimentalLanguageFeatures)
178-
public func warningGroupBehavior(
177+
public func warningGroupControl(
179178
at position: AbsolutePosition,
180179
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier
181-
) -> WarningGroupBehavior? {
182-
return rootRegionNode.innermostContainingRegion(at: position, for: diagnosticGroupIdentifier)?.behavior
180+
) -> WarningGroupControl? {
181+
return rootRegionNode.innermostContainingRegion(at: position, for: diagnosticGroupIdentifier)?.control
183182
}
184183
}
185184

186185
/// A node in the warning control region tree, representing a collection of warning
187186
/// group controls and references to its nested child regions.
188187
private class WarningControlRegionNode {
189188
let range: Range<AbsolutePosition>
190-
var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] = [:]
189+
var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
191190
var children: [WarningControlRegionNode] = []
192191

193192
init(
194193
range: Range<AbsolutePosition>,
195194
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
196-
behavior: WarningGroupBehavior
195+
control: WarningGroupControl
197196
) {
198197
self.range = range
199-
self.warningGroupControls = [diagnosticGroupIdentifier: behavior]
198+
self.warningGroupControls = [diagnosticGroupIdentifier: control]
200199
}
201200

202201
init(range: Range<AbsolutePosition>) {
@@ -207,20 +206,20 @@ private class WarningControlRegionNode {
207206
/// Add a region with the same bounds as this node
208207
func addWarningGroupControl(
209208
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
210-
behavior: WarningGroupBehavior
209+
control: WarningGroupControl
211210
) {
212-
warningGroupControls[diagnosticGroupIdentifier] = behavior
211+
warningGroupControls[diagnosticGroupIdentifier] = control
213212
}
214213

215214
/// Get region with specific identifier if it exists
216215
func getWarningGroupControl(for diagnosticGroupIdentifier: DiagnosticGroupIdentifier) -> WarningControlRegion? {
217-
guard let behaviorControl = warningGroupControls[diagnosticGroupIdentifier] else {
216+
guard let groupControl = warningGroupControls[diagnosticGroupIdentifier] else {
218217
return nil
219218
}
220219
return WarningControlRegion(
221220
range: range,
222221
diagnosticGroupIdentifier: diagnosticGroupIdentifier,
223-
behavior: behaviorControl
222+
control: groupControl
224223
)
225224
}
226225

Sources/SwiftWarningControl/WarningGroupBehavior.swift renamed to Sources/SwiftWarningControl/WarningGroupControl.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import SwiftSyntax
1414

1515
// Describes the emission behavior state of a particular warning diagnostic group.
1616
@_spi(ExperimentalLanguageFeatures)
17-
public enum WarningGroupBehavior: String {
17+
public enum WarningGroupControl: String {
1818
/// Emitted as a fatal error, halting compilation
1919
case error
2020
/// Emitted as a warning

Tests/SwiftWarningControlTest/WarningControlTests.swift

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,14 +228,66 @@ public class WarningGroupControlTests: XCTestCase {
228228
]
229229
)
230230
}
231+
232+
func testEnclosingGlobalControlOverride() throws {
233+
// Global control does not override syntactic control
234+
try assertWarningGroupControl(
235+
"""
236+
@warn(GroupID, as: error)
237+
func foo() {
238+
1️⃣let x = 1
239+
}
240+
""",
241+
globalControls: ["GroupID": .warning],
242+
diagnosticGroupID: "GroupID",
243+
states: [
244+
"1️⃣": .error
245+
]
246+
)
247+
248+
try assertWarningGroupControl(
249+
"""
250+
func foo() {
251+
1️⃣let x = 1
252+
@warn(GroupID, as: ignored)
253+
func bar() {
254+
2️⃣let x = 1
255+
}
256+
}
257+
""",
258+
globalControls: ["GroupID": .error],
259+
diagnosticGroupID: "GroupID",
260+
states: [
261+
"1️⃣": .error,
262+
"2️⃣": .ignored,
263+
]
264+
)
265+
}
266+
267+
func testEnclosingGlobalControlOnly() throws {
268+
// Global control used in absense of a syntactic control
269+
try assertWarningGroupControl(
270+
"""
271+
func foo() {
272+
1️⃣let x = 1
273+
}
274+
""",
275+
globalControls: ["GroupID": .warning],
276+
diagnosticGroupID: "GroupID",
277+
states: [
278+
"1️⃣": .warning
279+
]
280+
)
281+
}
231282
}
232283

233284
/// Assert that the various marked positions in the source code have the
234285
/// expected warning behavior controls.
235286
private func assertWarningGroupControl(
236287
_ markedSource: String,
288+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:],
237289
diagnosticGroupID: DiagnosticGroupIdentifier,
238-
states: [String: WarningGroupBehavior?],
290+
states: [String: WarningGroupControl?],
239291
file: StaticString = #filePath,
240292
line: UInt = #line
241293
) throws {
@@ -244,9 +296,6 @@ private func assertWarningGroupControl(
244296

245297
var parser = Parser(source)
246298
let tree = SourceFileSyntax.parse(from: &parser)
247-
248-
let warningControlRegions = tree.warningGroupControlRegionTree()
249-
250299
for (marker, location) in markerLocations {
251300
guard let expectedState = states[marker] else {
252301
XCTFail("Missing marker \(marker) in expected states", file: file, line: line)
@@ -259,13 +308,17 @@ private func assertWarningGroupControl(
259308
continue
260309
}
261310

262-
let groupBehavior = token.warningGroupBehavior(for: diagnosticGroupID)
263-
XCTAssertEqual(groupBehavior, expectedState)
311+
let warningControlRegions = tree.warningGroupControlRegionTree(globalControls: globalControls)
312+
let groupControl = token.warningGroupControl(
313+
for: diagnosticGroupID,
314+
globalControls: globalControls
315+
)
316+
XCTAssertEqual(groupControl, expectedState)
264317

265-
let groupBehaviorViaRegions = warningControlRegions.warningGroupBehavior(
318+
let groupControlViaRegions = warningControlRegions.warningGroupControl(
266319
at: absolutePosition,
267320
for: diagnosticGroupID
268321
)
269-
XCTAssertEqual(groupBehaviorViaRegions, expectedState)
322+
XCTAssertEqual(groupControlViaRegions, expectedState)
270323
}
271324
}

0 commit comments

Comments
 (0)