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