Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Sources/SwiftWarningControl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DiagnosticGroupIdentifier> = []
var recursionStack: Set<DiagnosticGroupIdentifier> = []
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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 18 additions & 5 deletions Sources/SwiftWarningControl/WarningControlRegionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -53,8 +62,12 @@ private class WarningControlRegionVisitor: SyntaxAnyVisitor {
var tree: WarningControlRegionTree
let containingPosition: AbsolutePosition?

init(_ topLevelRange: Range<AbsolutePosition>, containing position: AbsolutePosition? = nil) {
self.tree = WarningControlRegionTree(range: topLevelRange)
init(
_ topLevelRange: Range<AbsolutePosition>,
containing position: AbsolutePosition?,
groupInheritanceTree: DiagnosticGroupInheritanceTree?
) {
self.tree = WarningControlRegionTree(range: topLevelRange, groupInheritanceTree: groupInheritanceTree)
containingPosition = position
super.init(viewMode: .fixedUp)
}
Expand Down
25 changes: 22 additions & 3 deletions Sources/SwiftWarningControl/WarningControlRegions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,15 @@ public struct WarningControlRegionTree {
/// Root region representing top-level (file) scope
private var rootRegionNode: WarningControlRegionNode

init(range: Range<AbsolutePosition>) {
rootRegionNode = WarningControlRegionNode(range: range)
/// Inheritance tree among diagnostic group identifiers
let groupInheritanceTree: DiagnosticGroupInheritanceTree

init(
range: Range<AbsolutePosition>,
groupInheritanceTree: DiagnosticGroupInheritanceTree?
) {
self.rootRegionNode = WarningControlRegionNode(range: range)
self.groupInheritanceTree = groupInheritanceTree ?? DiagnosticGroupInheritanceTree()
}

/// Add a warning control region to the tree
Expand All @@ -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<DiagnosticGroupIdentifier> = []
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)
}
Expand Down
50 changes: 48 additions & 2 deletions Tests/SwiftWarningControlTest/WarningControlTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,52 @@ 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
/// expected warning behavior controls.
private func assertWarningGroupControl(
_ markedSource: String,
globalControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:],
groupInheritanceTree: DiagnosticGroupInheritanceTree? = nil,
diagnosticGroupID: DiagnosticGroupIdentifier,
states: [String: WarningGroupControl?],
file: StaticString = #filePath,
Expand All @@ -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)

Expand Down