Skip to content

Commit 98f40b1

Browse files
authored
Merge branch 'main' into closure-capture-syntax-node-update
2 parents 612109f + 829e487 commit 98f40b1

File tree

51 files changed

+1518
-748
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1518
-748
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ let package = Package(
143143

144144
.target(
145145
name: "SwiftIfConfig",
146-
dependencies: ["SwiftSyntax", "SwiftOperators"],
146+
dependencies: ["SwiftSyntax", "SwiftDiagnostics", "SwiftOperators"],
147147
exclude: ["CMakeLists.txt"]
148148
),
149149

Release Notes/601.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
- Description: This method translates an error into one or more diagnostics, recognizing `DiagnosticsError` and `DiagnosticMessage` instances or providing its own `Diagnostic` as needed.
1616
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816
1717

18+
- Added a new library `SwiftIfConfig`.
19+
- Description: This new library provides facilities for evaluating `#if` conditions and determining which regions of a syntax tree are active according to a given build configuration.
20+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/1816
21+
1822
## API Behavior Changes
1923

2024
- `SyntaxProtocol.trimmed` detaches the node
@@ -38,7 +42,13 @@
3842
- Description: Allows retrieving the radix value from the `literal.text`.
3943
- Issue: https://github.com/apple/swift-syntax/issues/405
4044
- Pull Request: https://github.com/apple/swift-syntax/pull/2605
41-
45+
46+
- `FixIt.Change` gained a new case `replaceChild(data:)`.
47+
- Description: The new case covers the replacement of a child node with another node.
48+
- Issue: https://github.com/swiftlang/swift-syntax/issues/2205
49+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2758
50+
- Migration steps: In exhaustive switches over `FixIt.Change`, cover the new case.
51+
4252
- `ClosureCaptureSyntax.name` is no longer optional.
4353
- Description: Due to the new `ClosureCaptureSyntax` node structure, `name` property is non-optional.
4454
- Pull request: https://github.com/swiftlang/swift-syntax/pull/2763

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,15 @@ extension PluginMessage.Diagnostic {
130130
to: .afterTrailingTrivia
131131
)
132132
text = newTrivia.description
133+
case .replaceChild(let replaceChildData):
134+
range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent)
135+
text = replaceChildData.newChild.description
133136
#if RESILIENT_LIBRARIES
134137
@unknown default:
135138
fatalError()
136139
#endif
137140
}
138-
guard let range = range else {
141+
guard let range else {
139142
return nil
140143
}
141144
return .init(

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,25 @@ class SourceManager {
169169
of node: Syntax,
170170
from startKind: PositionInSyntaxNode = .afterLeadingTrivia,
171171
to endKind: PositionInSyntaxNode = .beforeTrailingTrivia
172+
) -> SourceRange? {
173+
range(node.position(at: startKind)..<node.position(at: endKind), in: node)
174+
}
175+
176+
/// Get ``SourceRange`` (file name + UTF-8 offset range) of `localRange` in `node`'s root node, which must be one
177+
/// of the returned values from `add(_:)`.
178+
func range(
179+
_ localRange: @autoclosure () -> Range<AbsolutePosition>,
180+
in node: some SyntaxProtocol
172181
) -> SourceRange? {
173182
guard let base = self.knownSourceSyntax[node.root.id] else {
174183
return nil
175184
}
176-
let localStartPosition = node.position(at: startKind)
177-
let localEndPosition = node.position(at: endKind)
178-
precondition(localStartPosition <= localEndPosition)
179-
180185
let positionOffset = base.location.offset
181-
186+
let localRange = localRange()
182187
return SourceRange(
183188
fileName: base.location.fileName,
184-
startUTF8Offset: localStartPosition.advanced(by: positionOffset).utf8Offset,
185-
endUTF8Offset: localEndPosition.advanced(by: positionOffset).utf8Offset
189+
startUTF8Offset: localRange.lowerBound.advanced(by: positionOffset).utf8Offset,
190+
endUTF8Offset: localRange.upperBound.advanced(by: positionOffset).utf8Offset
186191
)
187192
}
188193

Sources/SwiftDiagnostics/Convenience.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,20 @@ extension FixIt {
5151
]
5252
)
5353
}
54+
55+
public static func replaceChild<Parent: SyntaxProtocol, Child: SyntaxProtocol>(
56+
message: FixItMessage,
57+
parent: Parent,
58+
replacingChildAt keyPath: WritableKeyPath<Parent, Child?> & Sendable,
59+
with newChild: Child
60+
) -> Self {
61+
FixIt(
62+
message: message,
63+
changes: [
64+
.replaceChild(
65+
data: FixIt.Change.ReplacingOptionalChildData(parent: parent, newChild: newChild, keyPath: keyPath)
66+
)
67+
]
68+
)
69+
}
5470
}

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,56 @@ public protocol FixItMessage: Sendable {
2727
var fixItID: MessageID { get }
2828
}
2929

30+
/// Types conforming to this protocol provide the data required for replacing a child node of a parent node.
31+
///
32+
/// Conforming types should ensure the child of ``parent`` to be replaced at ``replacementRange`` is type-compatible
33+
/// with ``newChild``. Conforming types are stored as type-erased existentials (i.e. `any ReplacingChildData`) in
34+
/// ``FixIt/Change/replaceChild(data:)`` to keep ``FixIt`` type-erased.
35+
public protocol ReplacingChildData: Sendable {
36+
associatedtype Parent: SyntaxProtocol
37+
associatedtype Child: SyntaxProtocol
38+
39+
/// The node whose child node at ``replacementRange`` to be replaced by ``newChild``.
40+
var parent: Parent { get }
41+
42+
/// The node to replace the child node of ``parent`` at ``replacementRange``.
43+
var newChild: Child { get }
44+
45+
/// The absolute position range of the child node to be replaced.
46+
///
47+
/// If a nil child node is to be replaced, conforming types should provide a zero-length range with both bounds
48+
/// denoting the start position of ``newChild`` in ``parent`` after replacement.
49+
var replacementRange: Range<AbsolutePosition> { get }
50+
}
51+
3052
/// A Fix-It that can be applied to resolve a diagnostic.
3153
public struct FixIt: Sendable {
3254
public enum Change: Sendable {
55+
struct ReplacingOptionalChildData<Parent: SyntaxProtocol, Child: SyntaxProtocol>: ReplacingChildData {
56+
let parent: Parent
57+
let newChild: Child
58+
let keyPath: WritableKeyPath<Parent, Child?> & Sendable
59+
60+
var replacementRange: Range<AbsolutePosition> {
61+
// need to upcast keyPath to strip Sendable for older Swift versions
62+
let keyPath: WritableKeyPath<Parent, Child?> = keyPath
63+
if let oldChild = parent[keyPath: keyPath] {
64+
return oldChild.range
65+
} else {
66+
let newChild = parent.with(keyPath, newChild)[keyPath: keyPath]!
67+
return newChild.position..<newChild.position
68+
}
69+
}
70+
}
71+
3372
/// Replace `oldNode` by `newNode`.
3473
case replace(oldNode: Syntax, newNode: Syntax)
3574
/// Replace the leading trivia on the given token
3675
case replaceLeadingTrivia(token: TokenSyntax, newTrivia: Trivia)
3776
/// Replace the trailing trivia on the given token
3877
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
78+
/// Replace the child node of the given parent node at the given replacement range with the given new child node
79+
case replaceChild(data: any ReplacingChildData)
3980
}
4081

4182
/// A description of what this Fix-It performs.
@@ -89,6 +130,12 @@ private extension FixIt.Change {
89130
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
90131
replacement: newTrivia.description
91132
)
133+
134+
case .replaceChild(let replacingChildData):
135+
return SourceEdit(
136+
range: replacingChildData.replacementRange,
137+
replacement: replacingChildData.newChild.description
138+
)
92139
}
93140
}
94141
}

Sources/SwiftIfConfig/ActiveSyntaxRewriter.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
//
14-
// This file defines the SyntaxRewriter, a class that performs a standard walk
15-
// and tree-rebuilding pattern.
16-
//
17-
// Subclassers of this class can override the walking behavior for any syntax
18-
// node and transform nodes however they like.
19-
//
20-
//===----------------------------------------------------------------------===//
21-
2213
import SwiftDiagnostics
2314
import SwiftSyntax
2415

@@ -34,15 +25,17 @@ extension SyntaxProtocol {
3425
/// clauses, e.g., `#if FOO > 10`, then the condition will be
3526
/// considered to have failed and the clauses's elements will be
3627
/// removed.
37-
public func removingInactive(in configuration: some BuildConfiguration) -> (Syntax, [Diagnostic]) {
28+
public func removingInactive(
29+
in configuration: some BuildConfiguration
30+
) -> (result: Syntax, diagnostics: [Diagnostic]) {
3831
// First pass: Find all of the active clauses for the #ifs we need to
3932
// visit, along with any diagnostics produced along the way. This process
4033
// does not change the tree in any way.
4134
let visitor = ActiveSyntaxVisitor(viewMode: .sourceAccurate, configuration: configuration)
4235
visitor.walk(self)
4336

4437
// If there were no active clauses to visit, we're done!
45-
if visitor.numIfClausesVisited == 0 {
38+
if !visitor.visitedAnyIfClauses {
4639
return (Syntax(self), visitor.diagnostics)
4740
}
4841

@@ -88,12 +81,13 @@ extension SyntaxProtocol {
8881
/// than trivia).
8982
class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
9083
let configuration: Configuration
84+
var diagnostics: [Diagnostic] = []
9185

9286
init(configuration: Configuration) {
9387
self.configuration = configuration
9488
}
9589

96-
private func dropInactive<List: Collection & SyntaxCollection>(
90+
private func dropInactive<List: SyntaxCollection>(
9791
_ node: List,
9892
elementAsIfConfig: (List.Element) -> IfConfigDeclSyntax?
9993
) -> List {
@@ -105,7 +99,10 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
10599
// Find #ifs within the list.
106100
if let ifConfigDecl = elementAsIfConfig(element) {
107101
// Retrieve the active `#if` clause
108-
let activeClause = ifConfigDecl.activeClause(in: configuration)
102+
let (activeClause, localDiagnostics) = ifConfigDecl.activeClause(in: configuration)
103+
104+
// Add these diagnostics.
105+
diagnostics.append(contentsOf: localDiagnostics)
109106

110107
// If this is the first element that changed, note that we have
111108
// changes and add all prior elements to the list of new elements.
@@ -255,7 +252,8 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
255252
return dropInactive(outerBase: base, postfixIfConfig: postfixIfConfig)
256253
}
257254

258-
preconditionFailure("Unhandled postfix expression in #if elimination")
255+
assertionFailure("Unhandled postfix expression in #if elimination")
256+
return postfix
259257
}
260258

261259
/// Drop inactive regions from a postfix `#if` configuration, applying the
@@ -265,7 +263,10 @@ class ActiveSyntaxRewriter<Configuration: BuildConfiguration>: SyntaxRewriter {
265263
postfixIfConfig: PostfixIfConfigExprSyntax
266264
) -> ExprSyntax {
267265
// Retrieve the active `if` clause.
268-
let activeClause = postfixIfConfig.config.activeClause(in: configuration)
266+
let (activeClause, localDiagnostics) = postfixIfConfig.config.activeClause(in: configuration)
267+
268+
// Record these diagnostics.
269+
diagnostics.append(contentsOf: localDiagnostics)
269270

270271
guard case .postfixExpression(let postfixExpr) = activeClause?.elements
271272
else {

Sources/SwiftIfConfig/ActiveSyntaxVisitor.swift

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
1010
//
1111
//===----------------------------------------------------------------------===//
12+
1213
import SwiftDiagnostics
1314
import SwiftSyntax
1415

@@ -36,34 +37,34 @@ import SwiftSyntax
3637
/// it would not visit either `f` or `g`.
3738
///
3839
/// All notes visited by this visitor will have the "active" state, i.e.,
39-
/// `node.isActive(in: configuration)` will evaluate to `.active` or will
40-
/// throw. When errors occur, they will be recorded in the set of
41-
/// diagnostics.
40+
/// `node.isActive(in: configuration)` will have evaluated to `.active`.
41+
/// When errors occur, they will be recorded in the array of diagnostics.
4242
open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor {
4343
/// The build configuration, which will be queried for each relevant `#if`.
4444
public let configuration: Configuration
4545

46-
/// The set of diagnostics accumulated during this walk of active syntax.
47-
public var diagnostics: [Diagnostic] = []
46+
/// The diagnostics accumulated during this walk of active syntax.
47+
public private(set) var diagnostics: [Diagnostic] = []
4848

49-
/// The number of "#if" clauses that were visited.
50-
var numIfClausesVisited: Int = 0
49+
/// Whether we visited any "#if" clauses.
50+
var visitedAnyIfClauses: Bool = false
5151

5252
public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) {
5353
self.configuration = configuration
5454
super.init(viewMode: viewMode)
5555
}
5656

5757
open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
58-
let activeClause = node.activeClause(in: configuration) { diag in
59-
self.diagnostics.append(diag)
60-
}
58+
// Note: there is a clone of this code in ActiveSyntaxAnyVisitor. If you
59+
// change one, please also change the other.
60+
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
61+
diagnostics.append(contentsOf: localDiagnostics)
6162

62-
numIfClausesVisited += 1
63+
visitedAnyIfClauses = true
6364

6465
// If there is an active clause, visit it's children.
6566
if let activeClause, let elements = activeClause.elements {
66-
walk(Syntax(elements))
67+
walk(elements)
6768
}
6869

6970
// Skip everything else in the #if.
@@ -95,32 +96,30 @@ open class ActiveSyntaxVisitor<Configuration: BuildConfiguration>: SyntaxVisitor
9596
/// it would not visit either `f` or `g`.
9697
///
9798
/// All notes visited by this visitor will have the "active" state, i.e.,
98-
/// `node.isActive(in: configuration)` will evaluate to `.active` or will
99-
/// throw.
100-
///
101-
/// All notes visited by this visitor will have the "active" state, i.e.,
102-
/// `node.isActive(in: configuration)` will evaluate to `.active` or will
103-
/// throw. When errors occur, they will be recorded in the set of
104-
/// diagnostivs.
99+
/// `node.isActive(in: configuration)` will have evaluated to `.active`.
100+
/// When errors occur, they will be recorded in the array of diagnostics.
105101
open class ActiveSyntaxAnyVisitor<Configuration: BuildConfiguration>: SyntaxAnyVisitor {
106102
/// The build configuration, which will be queried for each relevant `#if`.
107103
public let configuration: Configuration
108104

109-
/// The set of diagnostics accumulated during this walk of active syntax.
110-
public var diagnostics: [Diagnostic] = []
105+
/// The diagnostics accumulated during this walk of active syntax.
106+
public private(set) var diagnostics: [Diagnostic] = []
111107

112108
public init(viewMode: SyntaxTreeViewMode, configuration: Configuration) {
113109
self.configuration = configuration
114110
super.init(viewMode: viewMode)
115111
}
116112

117113
open override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
114+
// Note: there is a clone of this code in ActiveSyntaxVisitor. If you
115+
// change one, please also change the other.
116+
118117
// If there is an active clause, visit it's children.
119-
let activeClause = node.activeClause(in: configuration) { diag in
120-
self.diagnostics.append(diag)
121-
}
118+
let (activeClause, localDiagnostics) = node.activeClause(in: configuration)
119+
diagnostics.append(contentsOf: localDiagnostics)
120+
122121
if let activeClause, let elements = activeClause.elements {
123-
walk(Syntax(elements))
122+
walk(elements)
124123
}
125124

126125
// Skip everything else in the #if.

Sources/SwiftIfConfig/BuildConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public protocol BuildConfiguration {
253253
///
254254
/// The language version can be queried with the `swift` directive that checks
255255
/// how the supported language version compares, as described by
256-
/// [SE-0212](https://github.com/apple/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example:
256+
/// [SE-0212](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0212-compiler-version-directive.md). For example:
257257
///
258258
/// ```swift
259259
/// #if swift(>=5.5)

Sources/SwiftIfConfig/CMakeLists.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ add_swift_syntax_library(SwiftIfConfig
1111
ActiveSyntaxRewriter.swift
1212
BuildConfiguration.swift
1313
ConfiguredRegions.swift
14-
ConfiguredRegionState.swift
14+
IfConfigRegionState.swift
1515
IfConfigDecl+IfConfig.swift
1616
IfConfigError.swift
1717
IfConfigEvaluation.swift
@@ -25,5 +25,4 @@ add_swift_syntax_library(SwiftIfConfig
2525
target_link_swift_syntax_libraries(SwiftIfConfig PUBLIC
2626
SwiftSyntax
2727
SwiftDiagnostics
28-
SwiftOperators
29-
SwiftParser)
28+
SwiftOperators)

0 commit comments

Comments
 (0)