Skip to content

Commit f7a1d36

Browse files
committed
[SwiftWarningControl] Add support for file-scoped 'using @warn()'
These declarations may appear as top-level statements and affect the entire source file.
1 parent ca0480b commit f7a1d36

File tree

5 files changed

+138
-38
lines changed

5 files changed

+138
-38
lines changed

Sources/SwiftWarningControl/WarningControlDeclSyntax.swift

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,68 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import SwiftSyntax
13+
@_spi(ExperimentalLanguageFeatures) import SwiftSyntax
14+
15+
extension AttributeSyntax {
16+
var warningGroupControl: (DiagnosticGroupIdentifier, WarningGroupControl)? {
17+
// `@warn` attributes
18+
guard attributeName.as(IdentifierTypeSyntax.self)?.name.text == "warn"
19+
else {
20+
return nil
21+
}
22+
23+
// First argument is the unquoted diagnostic group identifier
24+
guard
25+
let diagnosticGroupID = arguments?
26+
.as(LabeledExprListSyntax.self)?.first?.expression
27+
.as(DeclReferenceExprSyntax.self)?.baseName.text
28+
else {
29+
return nil
30+
}
31+
32+
// Second argument is the `as: ` behavior control specifier
33+
guard
34+
let asParamExprSyntax = arguments?.as(LabeledExprListSyntax.self)?
35+
.dropFirst().first
36+
else {
37+
return nil
38+
}
39+
guard
40+
asParamExprSyntax.label?.text == "as",
41+
let controlText = asParamExprSyntax
42+
.expression.as(DeclReferenceExprSyntax.self)?
43+
.baseName.text,
44+
let control = WarningGroupControl(rawValue: controlText)
45+
else {
46+
return nil
47+
}
48+
49+
return (DiagnosticGroupIdentifier(diagnosticGroupID), control)
50+
}
51+
}
1452

1553
extension WithAttributesSyntax {
1654
/// Compute a dictionary of all `@warn` diagnostic group behavior controls
1755
/// specified on this warning control declaration scope.
1856
var allWarningGroupControls: [(DiagnosticGroupIdentifier, WarningGroupControl)] {
1957
attributes.reduce(into: [(DiagnosticGroupIdentifier, WarningGroupControl)]()) { result, attr in
20-
// `@warn` attributes
2158
guard case .attribute(let attributeSyntax) = attr,
22-
attributeSyntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "warn"
23-
else {
24-
return
25-
}
26-
27-
// First argument is the unquoted diagnostic group identifier
28-
guard
29-
let diagnosticGroupID = attributeSyntax.arguments?
30-
.as(LabeledExprListSyntax.self)?.first?.expression
31-
.as(DeclReferenceExprSyntax.self)?.baseName.text
59+
let warningGroupControl = attributeSyntax.warningGroupControl
3260
else {
3361
return
3462
}
63+
result.append(warningGroupControl)
64+
}
65+
}
66+
}
3567

36-
// Second argument is the `as: ` behavior control specifier
37-
guard
38-
let asParamExprSyntax = attributeSyntax
39-
.arguments?.as(LabeledExprListSyntax.self)?
40-
.dropFirst().first
41-
else {
42-
return
43-
}
44-
guard
45-
asParamExprSyntax.label?.text == "as",
46-
let controlText = asParamExprSyntax
47-
.expression.as(DeclReferenceExprSyntax.self)?
48-
.baseName.text,
49-
let control = WarningGroupControl(rawValue: controlText)
50-
else {
51-
return
52-
}
53-
result.append((DiagnosticGroupIdentifier(diagnosticGroupID), control))
68+
extension UsingDeclSyntax {
69+
var warningControl: (DiagnosticGroupIdentifier, WarningGroupControl)? {
70+
guard case .attribute(let attributeSyntax) = self.specifier,
71+
let warningGroupControl = attributeSyntax.warningGroupControl
72+
else {
73+
return nil
5474
}
75+
return warningGroupControl
5576
}
5677
}

Sources/SwiftWarningControl/WarningControlRegionBuilder.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import SwiftSyntax
13+
@_spi(ExperimentalLanguageFeatures) import SwiftSyntax
1414

1515
/// Compute the full set of warning control regions in this syntax node
1616
extension SyntaxProtocol {
@@ -73,14 +73,25 @@ private class WarningControlRegionVisitor: SyntaxAnyVisitor {
7373
}
7474

7575
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
76+
if let withAttributesSyntax = node.asProtocol(WithAttributesSyntax.self) {
77+
tree.addWarningControlRegions(for: withAttributesSyntax)
78+
}
79+
// Handle file-scoped `using` declarations before the `containingPosition`
80+
// check since they may only appear in top-level code and may affect
81+
// warning group control of all positions in this source file.
82+
if let usingAttributedSyntax = node.as(UsingDeclSyntax.self),
83+
let usingWarningControl = usingAttributedSyntax.warningControl
84+
{
85+
tree.addRootWarningGroupControls(controls: [usingWarningControl])
86+
}
87+
// Skip all declarations which do not contain the specified
88+
// `containingPosition`.
7689
if let containingPosition,
90+
node.isProtocol(DeclSyntaxProtocol.self),
7791
!node.range.contains(containingPosition)
7892
{
7993
return .skipChildren
8094
}
81-
if let withAttributesSyntax = node.asProtocol(WithAttributesSyntax.self) {
82-
tree.addWarningControlRegions(for: withAttributesSyntax)
83-
}
8495
return .visitChildren
8596
}
8697
}

Sources/SwiftWarningControl/WarningControlRegions.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ public struct WarningControlRegionTree {
149149
insertIntoSubtree(newNode, parent: rootRegionNode)
150150
}
151151

152+
/// Add warning control regions to the tree root node.
153+
/// For example, controls corresponding to a top-level `using @warn()` statement.
154+
mutating func addRootWarningGroupControls(controls: [(DiagnosticGroupIdentifier, WarningGroupControl)]) {
155+
addWarningGroupControls(range: rootRegionNode.range, controls: controls)
156+
}
157+
152158
/// Insert a region node into the appropriate position in a subtree.
153159
/// During top-down traversal of the syntax tree, nodes that are visited
154160
/// later should never contain any of the previously visited nodes,

Tests/SwiftParserTest/DeclarationTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3693,6 +3693,39 @@ final class UsingDeclarationTests: ParserTestCase {
36933693
)
36943694
)
36953695

3696+
assertParse(
3697+
"using @warn(DiagGroupID, as: warning)",
3698+
substructure: UsingDeclSyntax(
3699+
usingKeyword: .keyword(.using),
3700+
specifier: .attribute(
3701+
AttributeSyntax(
3702+
attributeName: IdentifierTypeSyntax(
3703+
name: .identifier("warn")
3704+
),
3705+
leftParen: .leftParenToken(),
3706+
arguments: .argumentList(
3707+
LabeledExprListSyntax([
3708+
LabeledExprSyntax(
3709+
expression: DeclReferenceExprSyntax(
3710+
baseName: .identifier("DiagGroupID")
3711+
),
3712+
trailingComma: .commaToken()
3713+
),
3714+
LabeledExprSyntax(
3715+
label: .identifier("as"),
3716+
colon: .colonToken(),
3717+
expression: DeclReferenceExprSyntax(
3718+
baseName: .identifier("warning")
3719+
)
3720+
),
3721+
])
3722+
),
3723+
rightParen: .rightParenToken()
3724+
)
3725+
)
3726+
)
3727+
)
3728+
36963729
assertParse(
36973730
"""
36983731
nonisolated

Tests/SwiftWarningControlTest/WarningControlTests.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
import SwiftParser
14-
import SwiftSyntax
13+
@_spi(ExperimentalLanguageFeatures) import SwiftParser
14+
@_spi(ExperimentalLanguageFeatures) import SwiftSyntax
1515
@_spi(ExperimentalLanguageFeatures) import SwiftWarningControl
1616
import XCTest
1717
import _SwiftSyntaxGenericTestSupport
@@ -387,6 +387,34 @@ public class WarningGroupControlTests: XCTestCase {
387387
]
388388
)
389389
}
390+
391+
func testFileScopeUsingWarningGroupControl() throws {
392+
try assertWarningGroupControl(
393+
"""
394+
0️⃣let x = 1
395+
@warn(GroupID, as: warning)
396+
struct Bar {
397+
1️⃣let y = 1
398+
}
399+
struct Foo {
400+
@warn(GroupID, as: ignored)
401+
var property: Int {
402+
2️⃣return 11
403+
}
404+
}
405+
using @warn(GroupID, as: error)
406+
3️⃣let k = 1
407+
""",
408+
experimentalFeatures: [.defaultIsolationPerFile],
409+
diagnosticGroupID: "GroupID",
410+
states: [
411+
"0️⃣": .error,
412+
"1️⃣": .warning,
413+
"2️⃣": .ignored,
414+
"3️⃣": .error
415+
]
416+
)
417+
}
390418
}
391419

392420
/// Assert that the various marked positions in the source code have the
@@ -395,15 +423,16 @@ private func assertWarningGroupControl(
395423
_ markedSource: String,
396424
globalControls: [(DiagnosticGroupIdentifier, WarningGroupControl)] = [],
397425
groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil,
426+
experimentalFeatures: Parser.ExperimentalFeatures? = nil,
398427
diagnosticGroupID: DiagnosticGroupIdentifier,
399428
states: [String: WarningGroupControl?],
400429
file: StaticString = #filePath,
401430
line: UInt = #line
402431
) throws {
403432
// Pull out the markers that we'll use to dig out nodes to query.
404433
let (markerLocations, source) = extractMarkers(markedSource)
405-
406-
var parser = Parser(source)
434+
let experimentalFeatures = experimentalFeatures ?? Parser.ExperimentalFeatures(rawValue: 0)
435+
var parser = Parser(source, experimentalFeatures: experimentalFeatures)
407436
let tree = SourceFileSyntax.parse(from: &parser)
408437
for (marker, location) in markerLocations {
409438
guard let expectedState = states[marker] else {

0 commit comments

Comments
 (0)