Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Sources/SwiftWarningControl/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftWarningControl/SwiftWarningControl.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
24 changes: 18 additions & 6 deletions Sources/SwiftWarningControl/SyntaxProtocol+WarningControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
14 changes: 7 additions & 7 deletions Sources/SwiftWarningControl/WarningControlDeclSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)?
Expand All @@ -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
}
}
}
12 changes: 9 additions & 3 deletions Sources/SwiftWarningControl/WarningControlRegionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
47 changes: 23 additions & 24 deletions Sources/SwiftWarningControl/WarningControlRegions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AbsolutePosition>
public let diagnosticGroupIdentifier: DiagnosticGroupIdentifier
public let behavior: WarningGroupBehavior
public let control: WarningGroupControl

init(
range: Range<AbsolutePosition>,
diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
behavior: WarningGroupBehavior
control: WarningGroupControl
) {
self.range = range
self.diagnosticGroupIdentifier = diagnosticGroupIdentifier
self.behavior = behavior
self.control = control
}
}

Expand Down Expand Up @@ -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
Expand All @@ -113,11 +110,12 @@ public struct WarningControlRegionTree {
/// Add a warning control region to the tree
mutating func addWarningGroupControls(
range: Range<AbsolutePosition>,
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)
}
Expand All @@ -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
}
Expand All @@ -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"
}
Expand All @@ -172,31 +171,31 @@ 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
}
}

/// 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<AbsolutePosition>
var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupBehavior] = [:]
var warningGroupControls: [DiagnosticGroupIdentifier: WarningGroupControl] = [:]
var children: [WarningControlRegionNode] = []

init(
range: Range<AbsolutePosition>,
for diagnosticGroupIdentifier: DiagnosticGroupIdentifier,
behavior: WarningGroupBehavior
control: WarningGroupControl
) {
self.range = range
self.warningGroupControls = [diagnosticGroupIdentifier: behavior]
self.warningGroupControls = [diagnosticGroupIdentifier: control]
}

init(range: Range<AbsolutePosition>) {
Expand All @@ -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
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 61 additions & 8 deletions Tests/SwiftWarningControlTest/WarningControlTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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)
}
}
Loading