Skip to content

Commit 829e487

Browse files
authored
Merge pull request #2758 from AppAppWorks/add-FixIt.Change-replace-child-of-syntax-node
Introduce FixIt.Change API that replaces the child of a syntax node
2 parents 50590d6 + 1c4c1cd commit 829e487

File tree

7 files changed

+99
-20
lines changed

7 files changed

+99
-20
lines changed

Release Notes/601.md

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
- Description: Allows retrieving the radix value from the `literal.text`.
3939
- Issue: https://github.com/apple/swift-syntax/issues/405
4040
- Pull Request: https://github.com/apple/swift-syntax/pull/2605
41+
42+
- `FixIt.Change` gained a new case `replaceChild(data:)`.
43+
- Description: The new case covers the replacement of a child node with another node.
44+
- Issue: https://github.com/swiftlang/swift-syntax/issues/2205
45+
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2758
46+
- Migration steps: In exhaustive switches over `FixIt.Change`, cover the new case.
4147

4248
## Template
4349

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

+4-1
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

+12-7
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

+16
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

+47
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/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift

+9
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,15 @@ fileprivate extension FixIt.Change {
623623
range: start..<end,
624624
replacement: newTrivia.description
625625
)
626+
627+
case .replaceChild(let replacingChildData):
628+
let range = replacingChildData.replacementRange
629+
let start = expansionContext.position(of: range.lowerBound, anchoredAt: replacingChildData.parent)
630+
let end = expansionContext.position(of: range.upperBound, anchoredAt: replacingChildData.parent)
631+
return SourceEdit(
632+
range: start..<end,
633+
replacement: replacingChildData.newChild.description
634+
)
626635
}
627636
}
628637
}

Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift

+5-12
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,17 @@ final class PeerMacroTests: XCTestCase {
4949
newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async))
5050
}
5151

52-
let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects)
53-
5452
let diag = Diagnostic(
5553
node: Syntax(funcDecl.funcKeyword),
5654
message: SwiftSyntaxMacros.MacroExpansionErrorMessage(
5755
"can only add a completion-handler variant to an 'async' function"
5856
),
5957
fixIts: [
60-
FixIt(
61-
message: SwiftSyntaxMacros.MacroExpansionFixItMessage(
62-
"add 'async'"
63-
),
64-
changes: [
65-
FixIt.Change.replace(
66-
oldNode: Syntax(funcDecl.signature),
67-
newNode: Syntax(newSignature)
68-
)
69-
]
58+
.replaceChild(
59+
message: SwiftSyntaxMacros.MacroExpansionFixItMessage("add 'async'"),
60+
parent: funcDecl,
61+
replacingChildAt: \.signature.effectSpecifiers,
62+
with: newEffects
7063
)
7164
]
7265
)

0 commit comments

Comments
 (0)