From a92c94cc6f25da03f0e619a24a71ac0e992eeb19 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Fri, 17 Oct 2025 17:07:02 -0700 Subject: [PATCH] Add 'SwiftWarningControl' library for source-level lexical warning group behavior settings This library provides support for a mechanism to control the behavior of specific diagnostic groups for a given declaration's lexical scope with the `@warn` attribute. The syntax tree and its parser do not reason about warning group controls. The syntax tree produced by the parser reprsents the `@warn` attribute in a generic fashion, as it would any other basic attribute on a declaration. The per-declaration nature of the attribute means that for any given declaration's lexical scope, the behavior of a given diagnosic group can be queried by checking for the presence of this attribute in its parent declaration scope. 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. This utility relies on constructing a warning control tree data structure to represent all warning control lexical scopes in a given source file. This data structure all of the `@warn` diagnostic group behavior controls within the given syntax node, indicating each group's active behavior at a given position. For example, given code like the following: ``` 1: @warn(Deprecate, as: error) 2: func foo() { 3: let a = dep 4: @warn(Deprecate, as: warning) 5: func bar() { 6: let b = dep 7: @warn(Deprecate, as: ignored) 8: func baz() { 9: let c = dep 10: } 11: @warn(Deprecate, as: error) 12: @warn(OtherGroup, as: ignored) 13: func qux() { 14: let d = dep 15: @warn(SomeOtherGroup, as: warning) 16: func corge() { 17: let e = dep 18: } 19: } 20: } 21: } 22: func grault() { 23: let f = dep 24: } ``` the result will be: ``` - [`Deprecate`:`error`] region within `foo` lexical scope (lines 1-22) - [`Deprecate`:`warning`] region within `bar` lexical scope (lines 4-20) - [`Deprecate`:`ignored`] region within `baz` lexical scope (lines 7-10) - [`Deprecate`:`error`, `OtherGroup`:`ignored`] region within `qux` lexical scope (lines 12-19) - [`SomeOtherGroup`:`warning`] region within `corge` lexical scope (lines 15-18) ``` The data structure represents these regions and their nesting relationships as an interval tree where interval nodes can only be nested or disjoint, and where a given interval corresponds to a diagnostic control region for one or more diagnostic group, with a behavior specifier for each. Intervals cannot partially overlap, and each node's child intervals are always kept sorted. The tree has multiple top-level nodes (roots) representing file-level warning control regions. Once the tree is computed, lookup of a diagnostic group behavior at a given position is performed by recursively descending into the child node containing the given position (located with a binary search of child nodes of a given parent node). Once the position's depth in the interval tree is reached, we walk back the traversal until we find the first containing region which specifies warning behavior control for the given diagnostic group id. --- .spi.yml | 1 + Package.swift | 19 ++ Sources/CMakeLists.txt | 1 + Sources/SwiftWarningControl/CMakeLists.txt | 19 ++ .../SwiftWarningControl.md | 30 ++ .../SyntaxProtocol+WarningControl.swift | 26 ++ .../WarningControlDeclSyntax.swift | 56 ++++ .../WarningControlRegionBuilder.swift | 67 ++++ .../WarningControlRegions.swift | 298 ++++++++++++++++++ .../WarningGroupBehavior.swift | 24 ++ .../WarningControlTests.swift | 271 ++++++++++++++++ 11 files changed, 812 insertions(+) create mode 100644 Sources/SwiftWarningControl/CMakeLists.txt create mode 100644 Sources/SwiftWarningControl/SwiftWarningControl.md create mode 100644 Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift create mode 100644 Sources/SwiftWarningControl/WarningControlDeclSyntax.swift create mode 100644 Sources/SwiftWarningControl/WarningControlRegionBuilder.swift create mode 100644 Sources/SwiftWarningControl/WarningControlRegions.swift create mode 100644 Sources/SwiftWarningControl/WarningGroupBehavior.swift create mode 100644 Tests/SwiftWarningControlTest/WarningControlTests.swift diff --git a/.spi.yml b/.spi.yml index 1118de8d5a6..6f03c01270f 100644 --- a/.spi.yml +++ b/.spi.yml @@ -18,6 +18,7 @@ builder: - SwiftParser - SwiftParserDiagnostics - SwiftRefactor + - SwiftWarningControl - SwiftSyntaxBuilder - SwiftSyntaxMacros - SwiftSyntaxMacroExpansion diff --git a/Package.swift b/Package.swift index 961508b8807..20e3fcec50c 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,7 @@ if buildDynamicLibrary { .library(name: "SwiftDiagnostics", targets: ["SwiftDiagnostics"]), .library(name: "SwiftIDEUtils", targets: ["SwiftIDEUtils"]), .library(name: "SwiftIfConfig", targets: ["SwiftIfConfig"]), + .library(name: "SwiftWarningControl", targets: ["SwiftWarningControl"]), .library(name: "SwiftLexicalLookup", targets: ["SwiftLexicalLookup"]), .library(name: "SwiftOperators", targets: ["SwiftOperators"]), .library(name: "SwiftParser", targets: ["SwiftParser"]), @@ -180,6 +181,24 @@ let package = Package( ] ), + // MARK: SwiftWarningControl + + .target( + name: "SwiftWarningControl", + dependencies: ["SwiftSyntax", "SwiftParser"], + exclude: ["CMakeLists.txt"] + ), + + .testTarget( + name: "SwiftWarningControlTest", + dependencies: [ + "_SwiftSyntaxTestSupport", + "SwiftWarningControl", + "SwiftParser", + "SwiftSyntaxMacrosGenericTestSupport", + ] + ), + // MARK: SwiftLexicalLookup .target( diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 7c00da25f0e..0b28ca19927 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -24,4 +24,5 @@ add_subdirectory(SwiftSyntaxMacroExpansion) add_subdirectory(SwiftCompilerPluginMessageHandling) add_subdirectory(SwiftIDEUtils) add_subdirectory(SwiftCompilerPlugin) +add_subdirectory(SwiftWarningControl) add_subdirectory(VersionMarkerModules) diff --git a/Sources/SwiftWarningControl/CMakeLists.txt b/Sources/SwiftWarningControl/CMakeLists.txt new file mode 100644 index 00000000000..d3f6467b5d9 --- /dev/null +++ b/Sources/SwiftWarningControl/CMakeLists.txt @@ -0,0 +1,19 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_swift_syntax_library(SwiftWarningControl + WarningGroupBehavior.swift + WarningControlDeclSyntax.swift + WarningControlRegionBuilder.swift + WarningControlRegions.swift + SyntaxProtocol+WarningControl.swift +) + +target_link_swift_syntax_libraries(SwiftWarningControl PUBLIC + SwiftSyntax + SwiftParser) diff --git a/Sources/SwiftWarningControl/SwiftWarningControl.md b/Sources/SwiftWarningControl/SwiftWarningControl.md new file mode 100644 index 00000000000..952ec6d0ec6 --- /dev/null +++ b/Sources/SwiftWarningControl/SwiftWarningControl.md @@ -0,0 +1,30 @@ +# SwiftWarningControl + +A library to evaluate `@warn` diagnostic group controls within a Swift syntax tree. + +## Overview + +Swift provides a mechanism to control the behavior of specific diagnostic groups for a given declaration's lexical scope with the `@warn` attribute. + +The syntax tree and its parser do not reason about warning group controls. The syntax tree produced by the parser represents the `@warn` attribute in a generic fashion, as it would any other basic attribute on a declaration. The per-declaration nature of the attribute means that for any given lexical scope, the behavior of a given diagnostic group can be queried by checking for the presence of this attribute in its parent declaration scope. + +```swift +@warn(Deprecate, as: error) +func foo() { + ... + @warn(Deprecate, as: warning) + func bar() { + ... + @warn("Deprecate", as: ignored, reason: "Foo") + func baz() { + ... + } + } +} +``` + +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.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. diff --git a/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift new file mode 100644 index 00000000000..846f1839ef9 --- /dev/null +++ b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftSyntax + +extension SyntaxProtocol { + /// Get the warning emission behavior for the specified diagnostic group + /// by determining its containing `WarningControlRegion`, if one is present. + @_spi(ExperimentalLanguageFeatures) + public func warningGroupBehavior( + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier + ) -> WarningGroupBehavior? { + let warningControlRegions = root.warningGroupControlRegionTreeImpl(containing: self.position) + return warningControlRegions.warningGroupBehavior(at: self.position, for: diagnosticGroupIdentifier) + } +} diff --git a/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift new file mode 100644 index 00000000000..4781144e9b5 --- /dev/null +++ b/Sources/SwiftWarningControl/WarningControlDeclSyntax.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension WithAttributesSyntax { + /// Compute a dictionary of all `@warn` diagnostic group behaviors + /// specified on this warning control declaration scope. + var allWarningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] { + attributes.reduce(into: [DiagnosticGroupIdentifier: WarningGroupBehavior]()) { 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 + else { + return + } + + // Second argument is the `as: ` behavior specifier + guard + let asParamExprSyntax = attributeSyntax + .arguments?.as(LabeledExprListSyntax.self)? + .dropFirst().first + else { + return + } + guard + asParamExprSyntax.label?.text == "as", + let behaviorText = asParamExprSyntax + .expression.as(DeclReferenceExprSyntax.self)? + .baseName.text, + let behavior = WarningGroupBehavior(rawValue: behaviorText) + else { + return + } + result[DiagnosticGroupIdentifier(diagnosticGroupID)] = behavior + } + } +} diff --git a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift new file mode 100644 index 00000000000..768184a03d7 --- /dev/null +++ b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Compute the full set of warning control regions in this syntax node +extension SyntaxProtocol { + @_spi(ExperimentalLanguageFeatures) + public func warningGroupControlRegionTree() -> WarningControlRegionTree { + return warningGroupControlRegionTreeImpl() + } + + /// 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 { + let visitor = WarningControlRegionVisitor(self.range, containing: position) + visitor.walk(self) + return visitor.tree + } +} + +/// Add this warning control decl syntax node warning group controls (as specified with `@warn`) +/// to the tree. +extension WarningControlRegionTree { + mutating func addWarningControlRegions(for syntax: some WithAttributesSyntax) { + addWarningGroupControls( + range: syntax.range, + controls: syntax.allWarningGroupControls + ) + } +} + +/// Helper class that walks a syntax tree looking for warning behavior control regions. +private class WarningControlRegionVisitor: SyntaxAnyVisitor { + /// The tree of warning control regions we have found so far + var tree: WarningControlRegionTree + let containingPosition: AbsolutePosition? + + init(_ topLevelRange: Range, containing position: AbsolutePosition? = nil) { + self.tree = WarningControlRegionTree(range: topLevelRange) + containingPosition = position + super.init(viewMode: .fixedUp) + } + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if let containingPosition, + !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 new file mode 100644 index 00000000000..1a3f1e94388 --- /dev/null +++ b/Sources/SwiftWarningControl/WarningControlRegions.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// A single warning control region, consisting of a start and end positions, +/// a diagnostic group identifier, and an emission behavior specifier. +@_spi(ExperimentalLanguageFeatures) +public struct WarningControlRegion { + public let range: Range + public let diagnosticGroupIdentifier: DiagnosticGroupIdentifier + public let behavior: WarningGroupBehavior + + init( + range: Range, + diagnosticGroupIdentifier: DiagnosticGroupIdentifier, + behavior: WarningGroupBehavior + ) { + self.range = range + self.diagnosticGroupIdentifier = diagnosticGroupIdentifier + self.behavior = behavior + } +} + +/// A struct representing a diagnostic group identifier string +@_spi(ExperimentalLanguageFeatures) +public struct DiagnosticGroupIdentifier: Hashable, Sendable, ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.identifier = value + } + public init(_ value: String) { + self.identifier = value + } + public let identifier: String +} + +/// Describes all of the `@warn` diagnostic group behavior controls within the +/// given syntax node, indicating each group's active behavior at a given position. +/// +/// For example, given code like the following: +/// +/// ``` +/// 1 | @warn(Deprecate, as: error) +/// 2 | func foo() { +/// 3 | let a = dep +/// 4 | @warn(Deprecate, as: warning) +/// 5 | func bar() { +/// 6 | let b = dep +/// 7 | @warn(Deprecate, as: ignored) +/// 8 | func baz() { +/// 9 | let c = dep +/// 10 | } +/// 11 | @warn(Deprecate, as: error) +/// 12 | @warn(OtherGroup, as: error) +/// 13 | func qux() { +/// 14 | let d = dep +/// 15 | @warn(SomeOtherGroup, as: warning) +/// 16 | func corge() { +/// 17 | let e = dep +/// 18 | } +/// 19 | } +/// 20 | } +/// 21 | } +/// 22 | func grault() { +/// 23 | let f = dep +/// 24 | } +/// ``` +/// +/// the result will be: +/// - [`Deprecate`:`error`] region within `foo` lexical scope (lines 1-22) +/// - [`Deprecate`:`warning`] region within `bar` lexical scope (lines 4-20) +/// - [`Deprecate`:`ignored`] region within `baz` lexical scope (lines 7-10) +/// - [`Deprecate`:`error`, `OtherGroup`:`error`] region within `qux` lexical scope (lines 12-19) +/// - [`SomeOtherGroup`:`warning`] region within `corge` lexical scope (lines 15-18) +/// +/// The data structure represents these regions and their nesting relationships +/// as an interval tree where interval nodes can only be nested or disjoint, +/// and where a given interval corresponds to a diagnostic control region for one +/// or more diagnostic group, with a behavior specifier for each. +/// +/// Intervals cannot partially overlap, and each node's child intervals are always kept +/// sorted. The tree has multiple top-level nodes (roots) representing file-level warning +/// control regions. +/// +/// Once the tree is computed, lookup of a diagnostic group behavior at a given position +/// is performed by recursively descending into the child node containing +/// the given position (located with a binary search of child nodes of a given parent node). +/// Once the position's depth in the interval tree is reached, we walk back the +/// 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 + private var rootRegionNode: WarningControlRegionNode + + init(range: Range) { + rootRegionNode = WarningControlRegionNode(range: range) + } + + /// Add a warning control region to the tree + mutating func addWarningGroupControls( + range: Range, + controls: [DiagnosticGroupIdentifier: WarningGroupBehavior] + ) { + let newNode = WarningControlRegionNode(range: range) + for (diagnosticGroupIdentifier, behavior) in controls { + newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, behavior: behavior) + } + insertIntoSubtree(newNode, parent: rootRegionNode) + } + + /// 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, + /// so it is sufficient to either find an existing child to insert this node + /// into, or add this node as a new child itself. + private func insertIntoSubtree( + _ node: WarningControlRegionNode, + parent: WarningControlRegionNode + ) { + // 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) + } + return + } + + // Check if this should be a child of one of parent's children + if let containingChild = parent.findChildContaining(node.range.lowerBound) { + insertIntoSubtree(node, parent: containingChild) + return + } + + // Add as direct child of parent + parent.addChild(node) + } +} + +extension WarningControlRegionTree: CustomDebugStringConvertible { + public var debugDescription: String { + var result = "Warning Group Control Region Tree:\n" + func printNode(_ node: WarningControlRegionNode, indent: Int) { + 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" + } else { + result += "\n" + } + for child in node.children { + printNode(child, indent: indent + 1) + } + } + + printNode(rootRegionNode, indent: 0) + return result + } +} + +extension WarningControlRegionTree { + /// Determine the warning group behavior at a specified position + /// for a given diagnostic group + @_spi(ExperimentalLanguageFeatures) + public func warningGroupBehavior( + at position: AbsolutePosition, + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier + ) -> WarningGroupBehavior? { + return rootRegionNode.innermostContainingRegion(at: position, for: diagnosticGroupIdentifier)?.behavior + } +} + +/// A node in the warning control region tree, representing a collection of warning +/// group controls and references to its nested child regions. +private class WarningControlRegionNode { + let range: Range + var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] = [:] + var children: [WarningControlRegionNode] = [] + + init( + range: Range, + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, + behavior: WarningGroupBehavior + ) { + self.range = range + self.warningGroupControls = [diagnosticGroupIdentifier: behavior] + } + + init(range: Range) { + self.range = range + self.warningGroupControls = [:] + } + + /// Add a region with the same bounds as this node + func addWarningGroupControl( + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, + behavior: WarningGroupBehavior + ) { + warningGroupControls[diagnosticGroupIdentifier] = behavior + } + + /// Get region with specific identifier if it exists + func getWarningGroupControl(for diagnosticGroupIdentifier: DiagnosticGroupIdentifier) -> WarningControlRegion? { + guard let behaviorControl = warningGroupControls[diagnosticGroupIdentifier] else { + return nil + } + return WarningControlRegion( + range: range, + diagnosticGroupIdentifier: diagnosticGroupIdentifier, + behavior: behaviorControl + ) + } + + /// Add a child region that is directly nested within this region + func addChild(_ node: WarningControlRegionNode) { + precondition(range.contains(node.range)) + children.append(node) + children.sort() + } +} + +extension WarningControlRegionNode { + /// Find the most deeply-nested warning control region with a given diagnostic group identifier + /// containing the specified position. + /// + /// Lookup complexity is `O(log n + d)` where `n` is the branching factor and `d` is the warning + /// control node depth of the queried position. + func innermostContainingRegion( + at position: AbsolutePosition, + for diagnosticGroupIdentifier: DiagnosticGroupIdentifier + ) -> WarningControlRegion? { + guard range.contains(position) else { + return nil + } + if let childRegionContainingPosition = findChildContaining(position), + let nestedResult = childRegionContainingPosition.innermostContainingRegion( + at: position, + for: diagnosticGroupIdentifier + ) + { + return nestedResult + } + // If no child region has this warning group, check self + return getWarningGroupControl(for: diagnosticGroupIdentifier) + } + + /// Binary search to find the exact child containing the specified position, + /// or `nil` if one is not found, meaning it is only the `self` node itself that contains the position. + func findChildContaining(_ position: AbsolutePosition) -> WarningControlRegionNode? { + guard range.contains(position) && !children.isEmpty else { return nil } + + var left = 0 + var right = children.count - 1 + + while left <= right { + let mid = (left + right) / 2 + let region = children[mid] + if region.range.lowerBound > position { + right = mid - 1 + } else if region.range.upperBound <= position { + left = mid + 1 + } else { + return region + } + } + return nil + } +} + +extension WarningControlRegionNode: Comparable { + static func == (lhs: WarningControlRegionNode, rhs: WarningControlRegionNode) -> Bool { + return lhs.range == rhs.range + } + + static func < (lhs: WarningControlRegionNode, rhs: WarningControlRegionNode) -> Bool { + if lhs.range.lowerBound != rhs.range.lowerBound { + return lhs.range.lowerBound < rhs.range.lowerBound + } + // For same start, larger interval comes first (reverse order by end) + // This can happen, for example, when the top-level region (source-file scope) + // also contains a warning control scope at the very beginning which + // also begins at position 0. + return lhs.range.upperBound > rhs.range.upperBound + } +} diff --git a/Sources/SwiftWarningControl/WarningGroupBehavior.swift b/Sources/SwiftWarningControl/WarningGroupBehavior.swift new file mode 100644 index 00000000000..90507f61cb5 --- /dev/null +++ b/Sources/SwiftWarningControl/WarningGroupBehavior.swift @@ -0,0 +1,24 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +// Describes the emission behavior state of a particular warning diagnostic group. +@_spi(ExperimentalLanguageFeatures) +public enum WarningGroupBehavior: String { + /// Emitted as a fatal error, halting compilation + case error + /// Emitted as a warning + case warning + /// Fully suppressed, i.e. diagnostic not emitted + case ignored +} diff --git a/Tests/SwiftWarningControlTest/WarningControlTests.swift b/Tests/SwiftWarningControlTest/WarningControlTests.swift new file mode 100644 index 00000000000..f2111283804 --- /dev/null +++ b/Tests/SwiftWarningControlTest/WarningControlTests.swift @@ -0,0 +1,271 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftParser +import SwiftSyntax +@_spi(ExperimentalLanguageFeatures) import SwiftWarningControl +import XCTest +import _SwiftSyntaxGenericTestSupport +import _SwiftSyntaxTestSupport + +public class WarningGroupControlTests: XCTestCase { + func testSimpleFunctionWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: error) + func foo() { + 1️⃣let x = 1 + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error + ] + ) + } + + func testNestedFunctionWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(SomeOtherGroup, as: warning) + @warn(GroupID, as: error) + @warn(YetAnotherGroup, as: warning) + func foo() { + 1️⃣let x = 1 + @warn(GroupID, as: warning) + func bar() { + 2️⃣let x = 1 + @warn(GroupID, as: ignored) + @warn(SomeOtherGroup, as: ignored) + func baz() { + 3️⃣let x = 1 + } + @warn(GroupID, as: error) + func qux() { + 4️⃣let x = 1 + @warn(SomeOtherGroup, as: warning) + func corge() { + 5️⃣let x = 1 + } + } + } + } + func grault() { + 6️⃣let x = 1 + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .warning, + "3️⃣": .ignored, + "4️⃣": .error, + "5️⃣": .error, + "6️⃣": nil, + ] + ) + } + + func testNominalDeclWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: error) + class Foo { + 1️⃣let x = 1 + } + @warn(GroupID, as: ignored) + struct Bar { + 2️⃣let x = 1 + } + @warn(GroupID, as: warning) + enum Baz { + 3️⃣let x = 1 + } + @warn(GroupID, as: error) + actor Qux { + 4️⃣let x = 1 + @warn(GroupID, as: ignored) + struct Quux { + 5️⃣let x = 1 + } + } + @warn(GroupID, as: warning) + protocol Proto { + 6️⃣let x = 1 + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .ignored, + "3️⃣": .warning, + "4️⃣": .error, + "5️⃣": .ignored, + "6️⃣": .warning, + ] + ) + } + + func testInitializerWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: error) + struct Foo { + 1️⃣let x = 1 + @warn(GroupID, as: ignored) + init { + 2️⃣let x = 1 + } + @warn(GroupID, as: warning) + deinit { + 3️⃣let x = 1 + } + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .ignored, + "3️⃣": .warning, + ] + ) + } + + func testExtensionWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: error) + extension Foo { + 1️⃣let x = 1 + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error + ] + ) + } + + func testImportWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: ignored) + import1️⃣ Foo + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .ignored + ] + ) + } + + func testSubscriptWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: ignored) + struct Foo { + @warn(GroupID, as: error) + subscript(index: Int) -> Value { + 1️⃣let x = 1 + } + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error + ] + ) + } + + func testComputedPropertyWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: ignored) + struct Foo { + @warn(GroupID, as: error) + var property: Int { + 1️⃣return 11 + } + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error + ] + ) + } + + func testAccessorWarningGroupControl() throws { + try assertWarningGroupControl( + """ + @warn(GroupID, as: ignored) + struct Foo { + var property: Int { + @warn(GroupID, as: error) + get { + 1️⃣return 11 + } + @warn(GroupID, as: warning) + set { + 2️⃣let x = 1 + } + } + } + """, + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .warning, + ] + ) + } +} + +/// Assert that the various marked positions in the source code have the +/// expected warning behavior controls. +private func assertWarningGroupControl( + _ markedSource: String, + diagnosticGroupID: DiagnosticGroupIdentifier, + states: [String: WarningGroupBehavior?], + file: StaticString = #filePath, + line: UInt = #line +) 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 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) + continue + } + + let absolutePosition = AbsolutePosition(utf8Offset: location) + guard let token = tree.token(at: absolutePosition) else { + XCTFail("Unable to find token at location \(location)", file: file, line: line) + continue + } + + let groupBehavior = token.warningGroupBehavior(for: diagnosticGroupID) + XCTAssertEqual(groupBehavior, expectedState) + + let groupBehaviorViaRegions = warningControlRegions.warningGroupBehavior( + at: absolutePosition, + for: diagnosticGroupID + ) + XCTAssertEqual(groupBehaviorViaRegions, expectedState) + } +}