Skip to content

Commit d65c034

Browse files
committed
introduced .replaceChild to FixIt.Change to allow replacing an optional child node with a proper node
- introduced `ReplacingChildData` as the type-erased payload for `.replaceChild`
1 parent 9869b70 commit d65c034

File tree

6 files changed

+88
-13
lines changed

6 files changed

+88
-13
lines changed

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,16 @@ extension PluginMessage.Diagnostic {
130130
to: .afterTrailingTrivia
131131
)
132132
text = newTrivia.description
133+
case .replaceChild(let replaceChildData):
134+
let localRange = replaceChildData.replacementRange
135+
range = sourceManager.range(of: replaceChildData.parent, localRange: localRange)
136+
text = replaceChildData.newChild.description
133137
#if RESILIENT_LIBRARIES
134138
@unknown default:
135139
fatalError()
136140
#endif
137141
}
138-
guard let range = range else {
142+
guard let range else {
139143
return nil
140144
}
141145
return .init(

Sources/SwiftCompilerPluginMessageHandling/PluginMacroExpansionContext.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,23 @@ class SourceManager {
186186
)
187187
}
188188

189+
func range(
190+
of node: some SyntaxProtocol,
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/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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,45 @@ public protocol FixItMessage: Sendable {
2727
var fixItID: MessageID { get }
2828
}
2929

30+
public protocol ReplacingChildData: Sendable {
31+
associatedtype Parent: SyntaxProtocol
32+
associatedtype Child: SyntaxProtocol
33+
34+
var replacementRange: Range<AbsolutePosition> { get }
35+
var parent: Parent { get }
36+
var newChild: Child { get }
37+
}
38+
3039
/// A Fix-It that can be applied to resolve a diagnostic.
3140
public struct FixIt: Sendable {
3241
public enum Change: Sendable {
42+
struct ReplacingOptionalChildData<Parent: SyntaxProtocol, Child: SyntaxProtocol>: ReplacingChildData {
43+
let parent: Parent
44+
let newChild: Child
45+
let keyPath: WritableKeyPath<Parent, Child?> & Sendable
46+
47+
var replacementRange: Range<AbsolutePosition> {
48+
if let oldChild = parent[keyPath: keyPath] {
49+
return oldChild.range
50+
} else {
51+
let newParent = parent.with(keyPath, newChild)
52+
if let previousToken = newParent[keyPath: keyPath]?.previousToken(viewMode: .all) {
53+
return previousToken.endPosition..<previousToken.endPosition
54+
} else {
55+
return parent.position..<parent.position
56+
}
57+
}
58+
}
59+
}
60+
3361
/// Replace `oldNode` by `newNode`.
3462
case replace(oldNode: Syntax, newNode: Syntax)
3563
/// Replace the leading trivia on the given token
3664
case replaceLeadingTrivia(token: TokenSyntax, newTrivia: Trivia)
3765
/// Replace the trailing trivia on the given token
3866
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
67+
/// Replace a child of a parent node
68+
case replaceChild(data: any ReplacingChildData)
3969
}
4070

4171
/// A description of what this Fix-It performs.
@@ -89,6 +119,12 @@ private extension FixIt.Change {
89119
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
90120
replacement: newTrivia.description
91121
)
122+
123+
case .replaceChild(let replacingChildData):
124+
return SourceEdit(
125+
range: replacingChildData.replacementRange,
126+
replacement: replacingChildData.newChild.description
127+
)
92128
}
93129
}
94130
}

Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift

Lines changed: 9 additions & 0 deletions
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 replacingChildData):
627+
let range = replacingChildData.replacementRange
628+
let start = expansionContext.position(of: range.lowerBound, anchoredAt: replacingChildData.parent)
629+
let end = expansionContext.position(of: range.upperBound, anchoredAt: replacingChildData.parent)
630+
return SourceEdit(
631+
range: start..<end,
632+
replacement: replacingChildData.newChild.description
633+
)
625634
}
626635
}
627636
}

Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift

Lines changed: 5 additions & 12 deletions
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)