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) } }