diff --git a/Sources/SwiftWarningControl/CMakeLists.txt b/Sources/SwiftWarningControl/CMakeLists.txt index 8c6d7b22f2a..f102e2358b9 100644 --- a/Sources/SwiftWarningControl/CMakeLists.txt +++ b/Sources/SwiftWarningControl/CMakeLists.txt @@ -7,6 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_swift_syntax_library(SwiftWarningControl + DiagnosticGroupInheritanceTree.swift WarningGroupControl.swift WarningControlDeclSyntax.swift WarningControlRegionBuilder.swift diff --git a/Sources/SwiftWarningControl/DiagnosticGroupInheritanceTree.swift b/Sources/SwiftWarningControl/DiagnosticGroupInheritanceTree.swift new file mode 100644 index 00000000000..c4066dc43f8 --- /dev/null +++ b/Sources/SwiftWarningControl/DiagnosticGroupInheritanceTree.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A struct wrapper for a diagnostic group inheritance tree +/// represented with a dictionary of a group identifier to an array +/// of its sub-group identifiers. +@_spi(ExperimentalLanguageFeatures) +public struct DiagnosticGroupInheritanceTree { + private let subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]] + public init(subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]]) throws { + self.subGroups = subGroups + if hasCycle() { + throw WarningControlError.groupInheritanceCycle + } + } + init() { + self.subGroups = [:] + } + + /// Diagnostic groups that inherit from `group`. + func subgroups(of group: DiagnosticGroupIdentifier) -> [DiagnosticGroupIdentifier] { subGroups[group] ?? [] } +} + +extension DiagnosticGroupInheritanceTree { + // Check the subgroup tree for possible cycles + func hasCycle() -> Bool { + var visited: Set = [] + var recursionStack: Set = [] + func hasCycleFromGroup(_ group: DiagnosticGroupIdentifier) -> Bool { + if visited.insert(group).inserted { + recursionStack.insert(group) + let subgroups = self.subgroups(of: group) + for subGroup in subgroups { + if recursionStack.contains(subGroup) { + return true + } else if !visited.contains(subGroup), hasCycleFromGroup(subGroup) { + return true + } + } + } + recursionStack.remove(group) + return false + } + + for group in subGroups.keys { + if !visited.contains(group), hasCycleFromGroup(group) { + return true + } + } + return false + } +} + +/// Describes the kinds of diagnostics that can occur when processing warning +/// group control queries. This is an Error-conforming type so we can throw errors when +/// needed. +@_spi(ExperimentalLanguageFeatures) +public enum WarningControlError: Error, CustomStringConvertible { + case groupInheritanceCycle + + public var description: String { + switch self { + case .groupInheritanceCycle: + return "cycle detected in the warning group inheritance hierarchy" + } + } +} diff --git a/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift index 2514bc8e18b..2b058d721a0 100644 --- a/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift +++ b/Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift @@ -27,10 +27,12 @@ extension SyntaxProtocol { @_spi(ExperimentalLanguageFeatures) public func warningGroupControl( for diagnosticGroupIdentifier: DiagnosticGroupIdentifier, - globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:] + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:], + groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil ) -> WarningGroupControl? { let warningControlRegions = root.warningGroupControlRegionTreeImpl( globalControls: globalControls, + groupInheritanceTree: groupInheritanceTree, containing: self.position ) return warningControlRegions.warningGroupControl(at: self.position, for: diagnosticGroupIdentifier) diff --git a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift index e956b3269fe..285864bd2ee 100644 --- a/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift +++ b/Sources/SwiftWarningControl/WarningControlRegionBuilder.swift @@ -16,9 +16,13 @@ import SwiftSyntax extension SyntaxProtocol { @_spi(ExperimentalLanguageFeatures) public func warningGroupControlRegionTree( - globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:] + globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:], + groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil ) -> WarningControlRegionTree { - return warningGroupControlRegionTreeImpl(globalControls: globalControls) + return warningGroupControlRegionTreeImpl( + globalControls: globalControls, + groupInheritanceTree: groupInheritanceTree + ) } /// Implementation of constructing a region tree with an optional parameter @@ -27,9 +31,14 @@ extension SyntaxProtocol { /// queries. func warningGroupControlRegionTreeImpl( globalControls: [DiagnosticGroupIdentifier: WarningGroupControl], + groupInheritanceTree: DiagnosticGroupInheritanceTree?, containing position: AbsolutePosition? = nil ) -> WarningControlRegionTree { - let visitor = WarningControlRegionVisitor(self.range, containing: position) + let visitor = WarningControlRegionVisitor( + self.range, + containing: position, + groupInheritanceTree: groupInheritanceTree + ) visitor.tree.addWarningGroupControls(range: self.range, controls: globalControls) visitor.walk(self) return visitor.tree @@ -53,8 +62,12 @@ private class WarningControlRegionVisitor: SyntaxAnyVisitor { var tree: WarningControlRegionTree let containingPosition: AbsolutePosition? - init(_ topLevelRange: Range, containing position: AbsolutePosition? = nil) { - self.tree = WarningControlRegionTree(range: topLevelRange) + init( + _ topLevelRange: Range, + containing position: AbsolutePosition?, + groupInheritanceTree: DiagnosticGroupInheritanceTree? + ) { + self.tree = WarningControlRegionTree(range: topLevelRange, groupInheritanceTree: groupInheritanceTree) containingPosition = position super.init(viewMode: .fixedUp) } diff --git a/Sources/SwiftWarningControl/WarningControlRegions.swift b/Sources/SwiftWarningControl/WarningControlRegions.swift index da9feaebd37..54c035daa3e 100644 --- a/Sources/SwiftWarningControl/WarningControlRegions.swift +++ b/Sources/SwiftWarningControl/WarningControlRegions.swift @@ -103,8 +103,15 @@ public struct WarningControlRegionTree { /// Root region representing top-level (file) scope private var rootRegionNode: WarningControlRegionNode - init(range: Range) { - rootRegionNode = WarningControlRegionNode(range: range) + /// Inheritance tree among diagnostic group identifiers + let groupInheritanceTree: DiagnosticGroupInheritanceTree + + init( + range: Range, + groupInheritanceTree: DiagnosticGroupInheritanceTree? + ) { + self.rootRegionNode = WarningControlRegionNode(range: range) + self.groupInheritanceTree = groupInheritanceTree ?? DiagnosticGroupInheritanceTree() } /// Add a warning control region to the tree @@ -115,7 +122,19 @@ public struct WarningControlRegionTree { guard !controls.isEmpty else { return } let newNode = WarningControlRegionNode(range: range) for (diagnosticGroupIdentifier, control) in controls { - newNode.addWarningGroupControl(for: diagnosticGroupIdentifier, control: control) + // Handle the control for the added diagnostic group + // and propagate it to all of its subgroups. + var groups: [DiagnosticGroupIdentifier] = [diagnosticGroupIdentifier] + var processedGroups: Set = [] + while !groups.isEmpty { + let groupIdentifier = groups.removeFirst() + processedGroups.insert(groupIdentifier) + newNode.addWarningGroupControl(for: groupIdentifier, control: control) + let newSubGroups = groupInheritanceTree.subgroups(of: groupIdentifier).filter { !processedGroups.contains($0) } + // Ensure we add a corresponding control to each direct and + // transitive sub-group of the one specified on this control. + groups.append(contentsOf: newSubGroups) + } } insertIntoSubtree(newNode, parent: rootRegionNode) } diff --git a/Tests/SwiftWarningControlTest/WarningControlTests.swift b/Tests/SwiftWarningControlTest/WarningControlTests.swift index 4322908d46a..ee07d4a5e5d 100644 --- a/Tests/SwiftWarningControlTest/WarningControlTests.swift +++ b/Tests/SwiftWarningControlTest/WarningControlTests.swift @@ -279,6 +279,44 @@ public class WarningGroupControlTests: XCTestCase { ] ) } + + func testSubGroupInheritance() throws { + try assertWarningGroupControl( + """ + @warn(SuperGroupID, as: error) + func foo() { + 1️⃣let x = 1 + } + @warn(SuperSuperGroupID, as: ignored) + func bar() { + 2️⃣let x = 1 + } + """, + groupInheritanceTree: DiagnosticGroupInheritanceTree(subGroups: [ + "SuperGroupID": ["GroupID"], + "SuperSuperGroupID": ["SuperGroupID"], + ]), + diagnosticGroupID: "GroupID", + states: [ + "1️⃣": .error, + "2️⃣": .ignored, + ] + ) + } + + func testInheritanceTreeCycle() throws { + XCTAssertThrowsError( + try DiagnosticGroupInheritanceTree(subGroups: [ + "SuperGroupID": ["GroupID"], + "GroupID": ["SuperGroupID"], + ]) + ) { (error: any Error) in + XCTAssertEqual( + error as? WarningControlError, + .groupInheritanceCycle + ) + } + } } /// Assert that the various marked positions in the source code have the @@ -286,6 +324,7 @@ public class WarningGroupControlTests: XCTestCase { private func assertWarningGroupControl( _ markedSource: String, globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:], + groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil, diagnosticGroupID: DiagnosticGroupIdentifier, states: [String: WarningGroupControl?], file: StaticString = #filePath, @@ -308,10 +347,17 @@ private func assertWarningGroupControl( continue } - let warningControlRegions = tree.warningGroupControlRegionTree(globalControls: globalControls) + let warningControlRegions = tree.warningGroupControlRegionTree( + globalControls: globalControls, + groupInheritanceTree: groupInheritanceTree + ) + + print(warningControlRegions.debugDescription) + let groupControl = token.warningGroupControl( for: diagnosticGroupID, - globalControls: globalControls + globalControls: globalControls, + groupInheritanceTree: groupInheritanceTree ) XCTAssertEqual(groupControl, expectedState)