Skip to content

Commit 8feabad

Browse files
committed
[SwiftWarningControl] Add ability to specify diagnostic group inheritance
This takes form of a new parameter: `groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil` on both: - `SyntaxProtocol.warningGroupBehavior` - `SyntaxProtocol.warningGroupControlRegionTree` Where `DiagnosticGroupInheritanceTree` is a wrapper for a dictionary where the key is a super-group and values are its sub-groups. Upon encountering a warning group control (`@warn`), its corresponding region is populated with its identifier and behavior, as well as the same corresponding behavior for each of its sub-groups, direct and transitive.
1 parent 90e6d96 commit 8feabad

File tree

6 files changed

+175
-11
lines changed

6 files changed

+175
-11
lines changed

Sources/SwiftWarningControl/CMakeLists.txt

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

99
add_swift_syntax_library(SwiftWarningControl
10+
DiagnosticGroupInheritanceTree.swift
1011
WarningGroupControl.swift
1112
WarningControlDeclSyntax.swift
1213
WarningControlRegionBuilder.swift
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 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+
/// A struct wrapper for a diagnostic group inheritance tree
14+
/// represented with a dictionary of a group identifier to an array
15+
/// of its sub-group identifiers.
16+
@_spi(ExperimentalLanguageFeatures)
17+
public struct DiagnosticGroupInheritanceTree {
18+
private let subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]]
19+
public init(subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]]) throws {
20+
self.subGroups = subGroups
21+
if hasCycle() {
22+
throw WarningControlError.groupInheritanceCycle
23+
}
24+
}
25+
init() {
26+
self.subGroups = [:]
27+
}
28+
29+
/// Diagnostic groups that inherit from `group`.
30+
func subgroups(of group: DiagnosticGroupIdentifier) -> [DiagnosticGroupIdentifier] { subGroups[group] ?? [] }
31+
}
32+
33+
extension DiagnosticGroupInheritanceTree {
34+
// Check the subgroup tree for possible cycles
35+
func hasCycle() -> Bool {
36+
var visited: Set<DiagnosticGroupIdentifier> = []
37+
var recursionStack: Set<DiagnosticGroupIdentifier> = []
38+
func hasCycleFromGroup(_ group: DiagnosticGroupIdentifier) -> Bool {
39+
if !visited.contains(group) {
40+
visited.insert(group);
41+
recursionStack.insert(group)
42+
var cycleFound: Bool = false
43+
let subgroups = self.subgroups(of: group)
44+
for subGroup in subgroups {
45+
if recursionStack.contains(subGroup) {
46+
cycleFound = true
47+
} else if !visited.contains(subGroup),
48+
hasCycleFromGroup(subGroup)
49+
{
50+
cycleFound = true
51+
}
52+
}
53+
if cycleFound {
54+
return true
55+
}
56+
}
57+
recursionStack.remove(group)
58+
return false
59+
}
60+
61+
for group in subGroups.keys {
62+
if !visited.contains(group),
63+
hasCycleFromGroup(group)
64+
{
65+
return true
66+
}
67+
}
68+
return false
69+
}
70+
}
71+
72+
/// Describes the kinds of diagnostics that can occur when processing warning
73+
/// group control queries. This is an Error-conforming type so we can throw errors when
74+
/// needed.
75+
@_spi(ExperimentalLanguageFeatures)
76+
public enum WarningControlError: Error, CustomStringConvertible {
77+
case groupInheritanceCycle
78+
79+
public var description: String {
80+
switch self {
81+
case .groupInheritanceCycle:
82+
return "cycle detected in the warning group inheritance hierarchy"
83+
}
84+
}
85+
}

Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ extension SyntaxProtocol {
2727
@_spi(ExperimentalLanguageFeatures)
2828
public func warningGroupControl(
2929
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
30-
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
30+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:],
31+
groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil
3132
) -> WarningGroupControl? {
3233
let warningControlRegions = root.warningGroupControlRegionTreeImpl(
3334
globalControls: globalControls,
35+
groupInheritanceTree: groupInheritanceTree,
3436
containing: self.position
3537
)
3638
return warningControlRegions.warningGroupControl(at: self.position, for: diagnosticGroupIdentifier)

Sources/SwiftWarningControl/WarningControlRegionBuilder.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ import SwiftSyntax
1616
extension SyntaxProtocol {
1717
@_spi(ExperimentalLanguageFeatures)
1818
public func warningGroupControlRegionTree(
19-
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
19+
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:],
20+
groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil
2021
) -> WarningControlRegionTree {
21-
return warningGroupControlRegionTreeImpl(globalControls: globalControls)
22+
return warningGroupControlRegionTreeImpl(
23+
globalControls: globalControls,
24+
groupInheritanceTree: groupInheritanceTree
25+
)
2226
}
2327

2428
/// Implementation of constructing a region tree with an optional parameter
@@ -27,9 +31,14 @@ extension SyntaxProtocol {
2731
/// queries.
2832
func warningGroupControlRegionTreeImpl(
2933
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl],
34+
groupInheritanceTree: DiagnosticGroupInheritanceTree?,
3035
containing position: AbsolutePosition? = nil
3136
) -> WarningControlRegionTree {
32-
let visitor = WarningControlRegionVisitor(self.range, containing: position)
37+
let visitor = WarningControlRegionVisitor(
38+
self.range,
39+
containing: position,
40+
groupInheritanceTree: groupInheritanceTree
41+
)
3342
visitor.tree.addWarningGroupControls(range: self.range, controls: globalControls)
3443
visitor.walk(self)
3544
return visitor.tree
@@ -53,8 +62,12 @@ private class WarningControlRegionVisitor: SyntaxAnyVisitor {
5362
var tree: WarningControlRegionTree
5463
let containingPosition: AbsolutePosition?
5564

56-
init(_ topLevelRange: Range<AbsolutePosition>, containing position: AbsolutePosition? = nil) {
57-
self.tree = WarningControlRegionTree(range: topLevelRange)
65+
init(
66+
_ topLevelRange: Range<AbsolutePosition>,
67+
containing position: AbsolutePosition?,
68+
groupInheritanceTree: DiagnosticGroupInheritanceTree?
69+
) {
70+
self.tree = WarningControlRegionTree(range: topLevelRange, groupInheritanceTree: groupInheritanceTree)
5871
containingPosition = position
5972
super.init(viewMode: .fixedUp)
6073
}

Sources/SwiftWarningControl/WarningControlRegions.swift

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,15 @@ public struct WarningControlRegionTree {
103103
/// Root region representing top-level (file) scope
104104
private var rootRegionNode: WarningControlRegionNode
105105

106-
init(range: Range<AbsolutePosition>) {
107-
rootRegionNode = WarningControlRegionNode(range: range)
106+
/// Inheritance tree among diagnostic group identifiers
107+
let groupInheritanceTree: DiagnosticGroupInheritanceTree
108+
109+
init(
110+
range: Range<AbsolutePosition>,
111+
groupInheritanceTree: DiagnosticGroupInheritanceTree?
112+
) {
113+
self.rootRegionNode = WarningControlRegionNode(range: range)
114+
self.groupInheritanceTree = groupInheritanceTree ?? DiagnosticGroupInheritanceTree()
108115
}
109116

110117
/// Add a warning control region to the tree
@@ -115,7 +122,19 @@ public struct WarningControlRegionTree {
115122
guard !controls.isEmpty else { return }
116123
let newNode = WarningControlRegionNode(range: range)
117124
for (diagnosticGroupIdentifier, control) in controls {
118-
newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control)
125+
// Handle the control for the added diagnostic group
126+
// and propagate it to all of its subgroups.
127+
var groups: [DiagnosticGroupIdentifier] = [diagnosticGroupIdentifier]
128+
var processedGroups: Set<DiagnosticGroupIdentifier> = []
129+
while !groups.isEmpty {
130+
let groupIdentifier = groups.removeFirst()
131+
processedGroups.insert(groupIdentifier)
132+
newNode.addWarningGroupControl(for: groupIdentifier, control: control)
133+
let newSubGroups = groupInheritanceTree.subgroups(of: groupIdentifier).filter { !processedGroups.contains($0) }
134+
// Ensure we add a corresponding control to each direct and
135+
// transitive sub-group of the one specified on this control.
136+
groups.append(contentsOf: newSubGroups)
137+
}
119138
}
120139
insertIntoSubtree(newNode, parent: rootRegionNode)
121140
}

Tests/SwiftWarningControlTest/WarningControlTests.swift

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,50 @@ public class WarningGroupControlTests: XCTestCase {
279279
]
280280
)
281281
}
282+
283+
func testSubGroupInheritance() throws {
284+
try assertWarningGroupControl(
285+
"""
286+
@warn(SuperGroupID, as: error)
287+
func foo() {
288+
1️⃣let x = 1
289+
}
290+
@warn(SuperSuperGroupID, as: ignored)
291+
func bar() {
292+
2️⃣let x = 1
293+
}
294+
""",
295+
groupInheritanceTree: DiagnosticGroupInheritanceTree(subGroups: [
296+
"SuperGroupID": ["GroupID"],
297+
"SuperSuperGroupID": ["SuperGroupID"],
298+
]),
299+
diagnosticGroupID: "GroupID",
300+
states: [
301+
"1️⃣": .error,
302+
"2️⃣": .ignored,
303+
]
304+
)
305+
}
306+
307+
func testInheritanceTreeCycle() throws {
308+
XCTAssertThrowsError(
309+
try DiagnosticGroupInheritanceTree(subGroups: [
310+
"SuperGroupID": ["GroupID"],
311+
"GroupID": ["SuperGroupID"],
312+
])
313+
) { (error: any Error) in
314+
XCTAssertEqual(
315+
error as? WarningControlError, .groupInheritanceCycle)
316+
}
317+
}
282318
}
283319

284320
/// Assert that the various marked positions in the source code have the
285321
/// expected warning behavior controls.
286322
private func assertWarningGroupControl(
287323
_ markedSource: String,
288324
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:],
325+
groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil,
289326
diagnosticGroupID: DiagnosticGroupIdentifier,
290327
states: [String: WarningGroupControl?],
291328
file: StaticString = #filePath,
@@ -308,10 +345,17 @@ private func assertWarningGroupControl(
308345
continue
309346
}
310347

311-
let warningControlRegions = tree.warningGroupControlRegionTree(globalControls: globalControls)
348+
let warningControlRegions = tree.warningGroupControlRegionTree(
349+
globalControls: globalControls,
350+
groupInheritanceTree: groupInheritanceTree
351+
)
352+
353+
print(warningControlRegions.debugDescription)
354+
312355
let groupControl = token.warningGroupControl(
313356
for: diagnosticGroupID,
314-
globalControls: globalControls
357+
globalControls: globalControls,
358+
groupInheritanceTree: groupInheritanceTree
315359
)
316360
XCTAssertEqual(groupControl, expectedState)
317361

0 commit comments

Comments
 (0)