diff --git a/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift index dbc406e45f9..49c90a3d155 100644 --- a/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift +++ b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift @@ -10,47 +10,68 @@ // //===----------------------------------------------------------------------===// -import SwiftSyntax +@_spi(ExperimentalLanguageFeatures) import SwiftSyntax + +extension AttributeSyntax { + var warningGroupControl: (DiagnosticGroupIdentifier, WarningGroupControl)? { + // `@warn` attributes + guard attributeName.as(IdentifierTypeSyntax.self)?.name.text == "warn" + else { + return nil + } + + // First argument is the unquoted diagnostic group identifier + guard + let diagnosticGroupID = arguments? + .as(LabeledExprListSyntax.self)?.first?.expression + .as(DeclReferenceExprSyntax.self)?.baseName.text + else { + return nil + } + + // Second argument is the `as: ` behavior control specifier + guard + let asParamExprSyntax = arguments?.as(LabeledExprListSyntax.self)? + .dropFirst().first + else { + return nil + } + guard + asParamExprSyntax.label?.text == "as", + let controlText = asParamExprSyntax + .expression.as(DeclReferenceExprSyntax.self)? + .baseName.text, + let control = WarningGroupControl(rawValue: controlText) + else { + return nil + } + + return (DiagnosticGroupIdentifier(diagnosticGroupID), control) + } +} extension WithAttributesSyntax { /// Compute a dictionary of all `@warn` diagnostic group behavior controls /// specified on this warning control declaration scope. var allWarningGroupControls: [(DiagnosticGroupIdentifier, WarningGroupControl)] { attributes.reduce(into: [(DiagnosticGroupIdentifier, WarningGroupControl)]()) { result, attr in - // `@warn` attributes guard case .attribute(let attributeSyntax) = attr, - attributeSyntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "warn" - else { - return - } - - // First argument is the unquoted diagnostic group identifier - guard - let diagnosticGroupID = attributeSyntax.arguments? - .as(LabeledExprListSyntax.self)?.first?.expression - .as(DeclReferenceExprSyntax.self)?.baseName.text + let warningGroupControl = attributeSyntax.warningGroupControl else { return } + result.append(warningGroupControl) + } + } +} - // Second argument is the `as: ` behavior control specifier - guard - let asParamExprSyntax = attributeSyntax - .arguments?.as(LabeledExprListSyntax.self)? - .dropFirst().first - else { - return - } - guard - asParamExprSyntax.label?.text == "as", - let controlText = asParamExprSyntax - .expression.as(DeclReferenceExprSyntax.self)? - .baseName.text, - let control = WarningGroupControl(rawValue: controlText) - else { - return - } - result.append((DiagnosticGroupIdentifier(diagnosticGroupID), control)) +extension UsingDeclSyntax { + var warningControl: (DiagnosticGroupIdentifier, WarningGroupControl)? { + guard case .attribute(let attributeSyntax) = self.specifier, + let warningGroupControl = attributeSyntax.warningGroupControl + else { + return nil } + return warningGroupControl } } diff --git a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift index fc156a262c7..a5eaaa69402 100644 --- a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift +++ b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -import SwiftSyntax +@_spi(ExperimentalLanguageFeatures) import SwiftSyntax /// Compute the full set of warning control regions in this syntax node extension SyntaxProtocol { @@ -73,14 +73,25 @@ private class WarningControlRegionVisitor: SyntaxAnyVisitor { } override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if let withAttributesSyntax = node.asProtocol(WithAttributesSyntax.self) { + tree.addWarningControlRegions(for: withAttributesSyntax) + } + // Handle file-scoped `using` declarations before the `containingPosition` + // check since they may only appear in top-level code and may affect + // warning group control of all positions in this source file. + if let usingAttributedSyntax = node.as(UsingDeclSyntax.self), + let usingWarningControl = usingAttributedSyntax.warningControl + { + tree.addRootWarningGroupControls(controls: [usingWarningControl]) + } + // Skip all declarations which do not contain the specified + // `containingPosition`. if let containingPosition, + node.isProtocol(DeclSyntaxProtocol.self), !node.range.contains(containingPosition) { return .skipChildren } - if let withAttributesSyntax = node.asProtocol(WithAttributesSyntax.self) { - tree.addWarningControlRegions(for: withAttributesSyntax) - } return .visitChildren } } diff --git a/Sources/SwiftWarningControl/WarningControlRegions.swift b/Sources/SwiftWarningControl/WarningControlRegions.swift index ce4d84b6c3d..ddf2f6da25c 100644 --- a/Sources/SwiftWarningControl/WarningControlRegions.swift +++ b/Sources/SwiftWarningControl/WarningControlRegions.swift @@ -149,6 +149,12 @@ public struct WarningControlRegionTree { insertIntoSubtree(newNode, parent: rootRegionNode) } + /// Add warning control regions to the tree root node. + /// For example, controls corresponding to a top-level `using @warn()` statement. + mutating func addRootWarningGroupControls(controls: [(DiagnosticGroupIdentifier, WarningGroupControl)]) { + addWarningGroupControls(range: rootRegionNode.range, controls: controls) + } + /// Insert a region node into the appropriate position in a subtree. /// During top-down traversal of the syntax tree, nodes that are visited /// later should never contain any of the previously visited nodes, diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index 44c95f9ed38..02ad3eb2c41 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -3693,6 +3693,39 @@ final class UsingDeclarationTests: ParserTestCase { ) ) + assertParse( + "using @warn(DiagGroupID, as: warning)", + substructure: UsingDeclSyntax( + usingKeyword: .keyword(.using), + specifier: .attribute( + AttributeSyntax( + attributeName: IdentifierTypeSyntax( + name: .identifier("warn") + ), + leftParen: .leftParenToken(), + arguments: .argumentList( + LabeledExprListSyntax([ + LabeledExprSyntax( + expression: DeclReferenceExprSyntax( + baseName: .identifier("DiagGroupID") + ), + trailingComma: .commaToken() + ), + LabeledExprSyntax( + label: .identifier("as"), + colon: .colonToken(), + expression: DeclReferenceExprSyntax( + baseName: .identifier("warning") + ) + ), + ]) + ), + rightParen: .rightParenToken() + ) + ) + ) + ) + assertParse( """ nonisolated diff --git a/Tests/SwiftWarningControlTest/WarningControlTests.swift b/Tests/SwiftWarningControlTest/WarningControlTests.swift index 8afa9baf0c1..1121a99ddd2 100644 --- a/Tests/SwiftWarningControlTest/WarningControlTests.swift +++ b/Tests/SwiftWarningControlTest/WarningControlTests.swift @@ -10,8 +10,8 @@ // //===----------------------------------------------------------------------===// -import SwiftParser -import SwiftSyntax +@_spi(ExperimentalLanguageFeatures) import SwiftParser +@_spi(ExperimentalLanguageFeatures) import SwiftSyntax @_spi(ExperimentalLanguageFeatures) import SwiftWarningControl import XCTest import _SwiftSyntaxGenericTestSupport @@ -387,6 +387,34 @@ public class WarningGroupControlTests: XCTestCase { ] ) } + + func testFileScopeUsingWarningGroupControl() throws { + try assertWarningGroupControl( + """ + 0️⃣let x = 1 + @warn(GroupID, as: warning) + struct Bar { + 1️⃣let y = 1 + } + struct Foo { + @warn(GroupID, as: ignored) + var property: Int { + 2️⃣return 11 + } + } + using @warn(GroupID, as: error) + 3️⃣let k = 1 + """, + experimentalFeatures: [.defaultIsolationPerFile], + diagnosticGroupID: "GroupID", + states: [ + "0️⃣": .error, + "1️⃣": .warning, + "2️⃣": .ignored, + "3️⃣": .error + ] + ) + } } /// Assert that the various marked positions in the source code have the @@ -395,6 +423,7 @@ private func assertWarningGroupControl( _ markedSource: String, globalControls: [(DiagnosticGroupIdentifier, WarningGroupControl)] = [], groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil, + experimentalFeatures: Parser.ExperimentalFeatures? = nil, diagnosticGroupID: DiagnosticGroupIdentifier, states: [String: WarningGroupControl?], file: StaticString = #filePath, @@ -402,8 +431,8 @@ private func assertWarningGroupControl( ) throws { // Pull out the markers that we'll use to dig out nodes to query. let (markerLocations, source) = extractMarkers(markedSource) - - var parser = Parser(source) + let experimentalFeatures = experimentalFeatures ?? Parser.ExperimentalFeatures(rawValue: 0) + var parser = Parser(source, experimentalFeatures: experimentalFeatures) let tree = SourceFileSyntax.parse(from: &parser) for (marker, location) in markerLocations { guard let expectedState = states[marker] else {