Skip to content

Commit da96362

Browse files
committed
added .replaceChild to FixIt.Change to allow replacing a nullable child node with a proper node
type erased keypath, parent node and child node type safe operation is performed inside the `replaceRange` closure modified `PeerMacroTests` to adopt `.replaceChild`
1 parent 9869b70 commit da96362

File tree

5 files changed

+66
-5
lines changed

5 files changed

+66
-5
lines changed

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

+4
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ extension PluginMessage.Diagnostic {
130130
to: .afterTrailingTrivia
131131
)
132132
text = newTrivia.description
133+
case .replaceChild(_, let parent, let child, let replacingRange):
134+
let localRange = replacingRange()
135+
range = sourceManager.range(of: parent, localRange: localRange)
136+
text = child.description
133137
#if RESILIENT_LIBRARIES
134138
@unknown default:
135139
fatalError()

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

+17
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,23 @@ class SourceManager {
186186
)
187187
}
188188

189+
func range(
190+
of node: Syntax,
191+
localRange: Range<AbsolutePosition>
192+
) -> SourceRange? {
193+
guard let base = self.knownSourceSyntax[node.root.id] else {
194+
return nil
195+
}
196+
197+
let positionOffset = base.location.offset
198+
199+
return SourceRange(
200+
fileName: base.location.fileName,
201+
startUTF8Offset: localRange.lowerBound.advanced(by: positionOffset).utf8Offset,
202+
endUTF8Offset: localRange.upperBound.advanced(by: positionOffset).utf8Offset
203+
)
204+
}
205+
189206
/// Get location of `node` in the known root nodes.
190207
/// The root node of `node` must be one of the returned value from `add(_:)`.
191208
func location(

Sources/SwiftDiagnostics/FixIt.swift

+32
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ public struct FixIt: Sendable {
3636
case replaceLeadingTrivia(token: TokenSyntax, newTrivia: Trivia)
3737
/// Replace the trailing trivia on the given token
3838
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
39+
/// Replace a child of a parent node
40+
case replaceChild(
41+
_ keyPath: AnyKeyPath & Sendable,
42+
parent: Syntax,
43+
child: Syntax,
44+
replacingRange: @Sendable () -> Range<AbsolutePosition>
45+
)
46+
47+
public init<P: SyntaxProtocol, C: SyntaxProtocol>(
48+
parent: P,
49+
replacingChildAt keyPath: WritableKeyPath<P, C?> & Sendable,
50+
with newChild: C
51+
) {
52+
let replacingRange: @Sendable () -> Range<AbsolutePosition> = {
53+
if let oldChild = parent[keyPath: keyPath] {
54+
return oldChild.range
55+
} else {
56+
let newParent = parent.with(keyPath, newChild)
57+
if let previousToken = newParent[keyPath: keyPath]?.previousToken(viewMode: .all) {
58+
return previousToken.endPosition..<previousToken.endPosition
59+
} else {
60+
return parent.position..<parent.position
61+
}
62+
}
63+
}
64+
self = .replaceChild(keyPath, parent: Syntax(parent), child: Syntax(newChild), replacingRange: replacingRange)
65+
}
3966
}
4067

4168
/// A description of what this Fix-It performs.
@@ -89,6 +116,11 @@ private extension FixIt.Change {
89116
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
90117
replacement: newTrivia.description
91118
)
119+
case .replaceChild(_, _, let child, let replacingRange):
120+
return SourceEdit(
121+
range: replacingRange(),
122+
replacement: child.description
123+
)
92124
}
93125
}
94126
}

Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift

+9
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,15 @@ fileprivate extension FixIt.Change {
622622
range: start..<end,
623623
replacement: newTrivia.description
624624
)
625+
626+
case .replaceChild(_, let parent, let child, let replacingRange):
627+
let range = replacingRange()
628+
let start = expansionContext.position(of: range.lowerBound, anchoredAt: parent)
629+
let end = expansionContext.position(of: range.upperBound, anchoredAt: parent)
630+
return SourceEdit(
631+
range: start..<end,
632+
replacement: child.description
633+
)
625634
}
626635
}
627636
}

Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift

+4-5
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ 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(
@@ -62,9 +60,10 @@ final class PeerMacroTests: XCTestCase {
6260
"add 'async'"
6361
),
6462
changes: [
65-
FixIt.Change.replace(
66-
oldNode: Syntax(funcDecl.signature),
67-
newNode: Syntax(newSignature)
63+
FixIt.Change(
64+
parent: funcDecl,
65+
replacingChildAt: \.signature.effectSpecifiers,
66+
with: newEffects
6867
)
6968
]
7069
)

0 commit comments

Comments
 (0)