From 804df14476230e2be4aa7720559659decf77fbfa Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Thu, 30 Oct 2025 10:41:02 -0700 Subject: [PATCH] [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. --- Sources/SwiftWarningControl/CMakeLists.txt | 2 +- .../SwiftWarningControl.md | 4 +- .../SyntaxProtocol+WarningControl.swift | 24 +++++-- .../WarningControlDeclSyntax.swift | 14 ++-- .../WarningControlRegionBuilder.swift | 12 +++- .../WarningControlRegions.swift | 47 +++++++------ ...havior.swift => WarningGroupControl.swift} | 2 +- .../WarningControlTests.swift | 69 ++++++++++++++++--- 8 files changed, 122 insertions(+), 52 deletions(-) rename Sources/SwiftWarningControl/{WarningGroupBehavior.swift => WarningGroupControl.swift} (95%) diff --git a/Sources/SwiftWarningControl/CMakeLists.txt b/Sources/SwiftWarningControl/CMakeLists.txt index d3f6467b5d9..8c6d7b22f2a 100644 --- a/Sources/SwiftWarningControl/CMakeLists.txt +++ b/Sources/SwiftWarningControl/CMakeLists.txt @@ -7,7 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_swift_syntax_library(SwiftWarningControl - WarningGroupBehavior.swift + WarningGroupControl.swift WarningControlDeclSyntax.swift WarningControlRegionBuilder.swift WarningControlRegions.swift diff --git a/Sources/SwiftWarningControl/SwiftWarningControl.md b/Sources/SwiftWarningControl/SwiftWarningControl.md index 952ec6d0ec6..965bd82dfc7 100644 --- a/Sources/SwiftWarningControl/SwiftWarningControl.md +++ b/Sources/SwiftWarningControl/SwiftWarningControl.md @@ -25,6 +25,6 @@ func foo() { 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. -* `SyntaxProtocol.getWarningGroupControl(for diagnosticGroupIdentifier:)` produces the behavior specifier (`WarningGroupBehavior`: `error`, `warning`, `ignored`) which applies at this node. +* `SyntaxProtocol.getWarningGroupControl(for diagnosticGroupIdentifier:)` produces the behavior control specifier (`WarningGroupControl`: `error`, `warning`, `ignored`) which applies at this node. -* `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. +* `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. diff --git a/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift index 846f1839ef9..2514bc8e18b 100644 --- a/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift +++ b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift @@ -14,13 +14,25 @@ import SwiftDiagnostics import SwiftSyntax extension SyntaxProtocol { - /// Get the warning emission behavior for the specified diagnostic group + /// Get the warning emission behavior control for the specified diagnostic group /// by determining its containing `WarningControlRegion`, if one is present. + /// Returns the syntactic control for the given diagnostic group, or `nil` if + /// there is not one. + /// - Parameters: + /// - for diagnosticGroupIdentifier: The identifier of the diagnostic group. + /// - globalControls: The global controls to consider, specified by the client (compiler) + /// representing module-wide diagnostic group emission configuration, for example + /// with `-Wwarning` and `-Werror` flags. These controls can be overriden at + /// finer-grained scopes with the `@warn` attribute. @_spi(ExperimentalLanguageFeatures) - public func warningGroupBehavior( - for diagnosticGroupIdentifier: DiagnosticGroupIdentifier - ) -> WarningGroupBehavior? { - let warningControlRegions = root.warningGroupControlRegionTreeImpl(containing: self.position) - return warningControlRegions.warningGroupBehavior(at: self.position, for: diagnosticGroupIdentifier) + public func warningGroupControl( + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:] + ) -> WarningGroupControl? { + let warningControlRegions = root.warningGroupControlRegionTreeImpl( + globalControls: globalControls, + containing: self.position + ) + return warningControlRegions.warningGroupControl(at: self.position, for: diagnosticGroupIdentifier) } } diff --git a/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift index 4781144e9b5..6480600645a 100644 --- a/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift +++ b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift @@ -13,10 +13,10 @@ import SwiftSyntax extension WithAttributesSyntax { - /// Compute a dictionary of all `@warn` diagnostic group behaviors + /// Compute a dictionary of all `@warn` diagnostic group behavior controls /// specified on this warning control declaration scope. - var allWarningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] { - attributes.reduce(into: [DiagnosticGroupIdentifier: WarningGroupBehavior]()) { result, attr in + 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" @@ -33,7 +33,7 @@ extension WithAttributesSyntax { return } - // Second argument is the `as: ` behavior specifier + // Second argument is the `as: ` behavior control specifier guard let asParamExprSyntax = attributeSyntax .arguments?.as(LabeledExprListSyntax.self)? @@ -43,14 +43,14 @@ extension WithAttributesSyntax { } guard asParamExprSyntax.label?.text == "as", - let behaviorText = asParamExprSyntax + let controlText = asParamExprSyntax .expression.as(DeclReferenceExprSyntax.self)? .baseName.text, - let behavior = WarningGroupBehavior(rawValue: behaviorText) + let control = WarningGroupControl(rawValue: controlText) else { return } - result[DiagnosticGroupIdentifier(diagnosticGroupID)] = behavior + result[DiagnosticGroupIdentifier(diagnosticGroupID)] = control } } } diff --git a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift index 768184a03d7..e956b3269fe 100644 --- a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift +++ b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift @@ -15,16 +15,22 @@ import SwiftSyntax /// Compute the full set of warning control regions in this syntax node extension SyntaxProtocol { @_spi(ExperimentalLanguageFeatures) - public func warningGroupControlRegionTree() -> WarningControlRegionTree { - return warningGroupControlRegionTreeImpl() + public func warningGroupControlRegionTree( + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:] + ) -> WarningControlRegionTree { + return warningGroupControlRegionTreeImpl(globalControls: globalControls) } /// Implementation of constructing a region tree with an optional parameter /// to specify that the constructed tree must only contain nodes which contain /// a specific absolute position - meant to speed up tree generation for individual /// queries. - func warningGroupControlRegionTreeImpl(containing position: AbsolutePosition? = nil) -> WarningControlRegionTree { + func warningGroupControlRegionTreeImpl( + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl], + containing position: AbsolutePosition? = nil + ) -> WarningControlRegionTree { let visitor = WarningControlRegionVisitor(self.range, containing: position) + visitor.tree.addWarningGroupControls(range: self.range, controls: globalControls) visitor.walk(self) return visitor.tree } diff --git a/Sources/SwiftWarningControl/WarningControlRegions.swift b/Sources/SwiftWarningControl/WarningControlRegions.swift index 1a3f1e94388..da9feaebd37 100644 --- a/Sources/SwiftWarningControl/WarningControlRegions.swift +++ b/Sources/SwiftWarningControl/WarningControlRegions.swift @@ -13,21 +13,21 @@ import SwiftSyntax /// A single warning control region, consisting of a start and end positions, -/// a diagnostic group identifier, and an emission behavior specifier. +/// a diagnostic group identifier, and an emission behavior control specifier. @_spi(ExperimentalLanguageFeatures) public struct WarningControlRegion { public let range: Range public let diagnosticGroupIdentifier: DiagnosticGroupIdentifier - public let behavior: WarningGroupBehavior + public let control: WarningGroupControl init( range: Range, diagnosticGroupIdentifier: DiagnosticGroupIdentifier, - behavior: WarningGroupBehavior + control: WarningGroupControl ) { self.range = range self.diagnosticGroupIdentifier = diagnosticGroupIdentifier - self.behavior = behavior + self.control = control } } @@ -98,9 +98,6 @@ public struct DiagnosticGroupIdentifier: Hashable, Sendable, ExpressibleByString /// traversal until we find the first containing region which specifies warning /// behavior control for the given diagnostic group id. /// -/// TODO: Capture global configuration from command-line arguments -/// to represent global rules, such as `-Werror`, `-Wwarning`, -/// and `-suppress-warnings` as *the* root region node. @_spi(ExperimentalLanguageFeatures) public struct WarningControlRegionTree { /// Root region representing top-level (file) scope @@ -113,11 +110,12 @@ public struct WarningControlRegionTree { /// Add a warning control region to the tree mutating func addWarningGroupControls( range: Range, - controls: [DiagnosticGroupIdentifier: WarningGroupBehavior] + controls: [DiagnosticGroupIdentifier: WarningGroupControl] ) { + guard !controls.isEmpty else { return } let newNode = WarningControlRegionNode(range: range) - for (diagnosticGroupIdentifier, behavior) in controls { - newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, behavior: behavior) + for (diagnosticGroupIdentifier, control) in controls { + newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control) } insertIntoSubtree(newNode, parent: rootRegionNode) } @@ -134,7 +132,7 @@ public struct WarningControlRegionTree { // Check if the new region has the same boundaries as the parent if parent.range == node.range { for (diagnosticGroupIdentifier, control) in node.warningGroupControls { - parent.addWarningGroupControl(for: diagnosticGroupIdentifier, behavior: control) + parent.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control) } return } @@ -157,7 +155,8 @@ extension WarningControlRegionTree: CustomDebugStringConvertible { let spacing = String(repeating: " ", count: indent) result += "\(spacing)[\(node.range.lowerBound), \(node.range.upperBound)]" if !node.warningGroupControls.isEmpty { - result += " id(s): \(node.warningGroupControls.keys.map { $0.identifier }.joined(separator: ", "))\n" + result += + " control(s): \(node.warningGroupControls.map { $0.key.identifier + ": " + $0.value.rawValue }.joined(separator: ", "))\n" } else { result += "\n" } @@ -172,14 +171,14 @@ extension WarningControlRegionTree: CustomDebugStringConvertible { } extension WarningControlRegionTree { - /// Determine the warning group behavior at a specified position - /// for a given diagnostic group + /// Determine the warning group behavior control at a specified position + /// for a given diagnostic group. @_spi(ExperimentalLanguageFeatures) - public func warningGroupBehavior( + public func warningGroupControl( at position: AbsolutePosition, for diagnosticGroupIdentifier: DiagnosticGroupIdentifier - ) -> WarningGroupBehavior? { - return rootRegionNode.innermostContainingRegion(at: position, for: diagnosticGroupIdentifier)?.behavior + ) -> WarningGroupControl? { + return rootRegionNode.innermostContainingRegion(at: position, for: diagnosticGroupIdentifier)?.control } } @@ -187,16 +186,16 @@ extension WarningControlRegionTree { /// group controls and references to its nested child regions. private class WarningControlRegionNode { let range: Range - var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] = [:] + var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:] var children: [WarningControlRegionNode] = [] init( range: Range, for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, - behavior: WarningGroupBehavior + control: WarningGroupControl ) { self.range = range - self.warningGroupControls = [diagnosticGroupIdentifier: behavior] + self.warningGroupControls = [diagnosticGroupIdentifier: control] } init(range: Range) { @@ -207,20 +206,20 @@ private class WarningControlRegionNode { /// Add a region with the same bounds as this node func addWarningGroupControl( for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, - behavior: WarningGroupBehavior + control: WarningGroupControl ) { - warningGroupControls[diagnosticGroupIdentifier] = behavior + warningGroupControls[diagnosticGroupIdentifier] = control } /// Get region with specific identifier if it exists func getWarningGroupControl(for diagnosticGroupIdentifier: DiagnosticGroupIdentifier) -> WarningControlRegion? { - guard let behaviorControl = warningGroupControls[diagnosticGroupIdentifier] else { + guard let groupControl = warningGroupControls[diagnosticGroupIdentifier] else { return nil } return WarningControlRegion( range: range, diagnosticGroupIdentifier: diagnosticGroupIdentifier, - behavior: behaviorControl + control: groupControl ) } diff --git a/Sources/SwiftWarningControl/WarningGroupBehavior.swift b/Sources/SwiftWarningControl/WarningGroupControl.swift similarity index 95% rename from Sources/SwiftWarningControl/WarningGroupBehavior.swift rename to Sources/SwiftWarningControl/WarningGroupControl.swift index 90507f61cb5..d0e9a4d2b7b 100644 --- a/Sources/SwiftWarningControl/WarningGroupBehavior.swift +++ b/Sources/SwiftWarningControl/WarningGroupControl.swift @@ -14,7 +14,7 @@ import SwiftSyntax // Describes the emission behavior state of a particular warning diagnostic group. @_spi(ExperimentalLanguageFeatures) -public enum WarningGroupBehavior: String { +public enum WarningGroupControl: String { /// Emitted as a fatal error, halting compilation case error /// Emitted as a warning diff --git a/Tests/SwiftWarningControlTest/WarningControlTests.swift b/Tests/SwiftWarningControlTest/WarningControlTests.swift index f2111283804..4322908d46a 100644 --- a/Tests/SwiftWarningControlTest/WarningControlTests.swift +++ b/Tests/SwiftWarningControlTest/WarningControlTests.swift @@ -228,14 +228,66 @@ public class WarningGroupControlTests: XCTestCase { ] ) } + + func testEnclosingGlobalControlOverride() throws { + // Global control does not override syntactic control + try assertWarningGroupControl( + """ + @warn(GroupID, as: error) + func foo() { + 1️⃣let x = 1 + } + """, + globalControls: ["GroupID": .warning], + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error + ] + ) + + try assertWarningGroupControl( + """ + func foo() { + 1️⃣let x = 1 + @warn(GroupID, as: ignored) + func bar() { + 2️⃣let x = 1 + } + } + """, + globalControls: ["GroupID": .error], + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .ignored, + ] + ) + } + + func testEnclosingGlobalControlOnly() throws { + // Global control used in absense of a syntactic control + try assertWarningGroupControl( + """ + func foo() { + 1️⃣let x = 1 + } + """, + globalControls: ["GroupID": .warning], + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .warning + ] + ) + } } /// Assert that the various marked positions in the source code have the /// expected warning behavior controls. private func assertWarningGroupControl( _ markedSource: String, + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:], diagnosticGroupID: DiagnosticGroupIdentifier, - states: [String: WarningGroupBehavior?], + states: [String: WarningGroupControl?], file: StaticString = #filePath, line: UInt = #line ) throws { @@ -244,9 +296,6 @@ private func assertWarningGroupControl( var parser = Parser(source) let tree = SourceFileSyntax.parse(from: &parser) - - let warningControlRegions = tree.warningGroupControlRegionTree() - for (marker, location) in markerLocations { guard let expectedState = states[marker] else { XCTFail("Missing marker \(marker) in expected states", file: file, line: line) @@ -259,13 +308,17 @@ private func assertWarningGroupControl( continue } - let groupBehavior = token.warningGroupBehavior(for: diagnosticGroupID) - XCTAssertEqual(groupBehavior, expectedState) + let warningControlRegions = tree.warningGroupControlRegionTree(globalControls: globalControls) + let groupControl = token.warningGroupControl( + for: diagnosticGroupID, + globalControls: globalControls + ) + XCTAssertEqual(groupControl, expectedState) - let groupBehaviorViaRegions = warningControlRegions.warningGroupBehavior( + let groupControlViaRegions = warningControlRegions.warningGroupControl( at: absolutePosition, for: diagnosticGroupID ) - XCTAssertEqual(groupBehaviorViaRegions, expectedState) + XCTAssertEqual(groupControlViaRegions, expectedState) } }