Skip to content

Commit 1bd7cd4

Browse files
committed
Adjust locations for edits based on detached nodes when computing fixedSource
The Fix-Its returned by the macro are defined in terms of the nodes that it got passed and thus their positions are relative to the start of the detached nodes that get passed to the macro. We need to translate these offsets back to the the offsets in the original source file before computing edits and apply the offset-based edits. Fixes #2332 rdar://118012820
1 parent b73118c commit 1bd7cd4

File tree

4 files changed

+109
-4
lines changed

4 files changed

+109
-4
lines changed

Package.swift

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

9494
.target(
9595
name: "_SwiftSyntaxTestSupport",
96-
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder"]
96+
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacroExpansion"]
9797
),
9898

9999
.testTarget(

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,16 @@ public func assertMacroExpansion(
325325

326326
// Applying Fix-Its
327327
if let expectedFixedSource = expectedFixedSource {
328-
let fixedTree = FixItApplier.applyFixes(from: context.diagnostics, filterByMessages: applyFixIts, to: origSourceFile)
328+
let messages = applyFixIts ?? context.diagnostics.compactMap { $0.fixIts.first?.message.message }
329+
330+
let edits =
331+
context.diagnostics
332+
.flatMap(\.fixIts)
333+
.filter { messages.contains($0.message.message) }
334+
.flatMap { $0.changes }
335+
.map { $0.edit(in: context) }
336+
337+
let fixedTree = FixItApplier.apply(edits: edits, to: origSourceFile)
329338
let fixedTreeDescription = fixedTree.description
330339
assertStringsEqualWithDiff(
331340
fixedTreeDescription.trimmingTrailingWhitespace(),
@@ -335,3 +344,50 @@ public func assertMacroExpansion(
335344
)
336345
}
337346
}
347+
348+
fileprivate extension FixIt.Change {
349+
/// Returns the edit for this change, translating positions from detached nodes
350+
/// to the corresponding locations in the original source file based on
351+
/// `expansionContext`.
352+
///
353+
/// - SeeAlso: `FixIt.Change.edit`
354+
func edit(in expansionContext: BasicMacroExpansionContext) -> SourceEdit {
355+
switch self {
356+
case .replace(let oldNode, let newNode):
357+
let start = expansionContext.position(of: oldNode.position, anchoredAt: oldNode)
358+
let end = expansionContext.position(of: oldNode.endPosition, anchoredAt: oldNode)
359+
return SourceEdit(
360+
range: start..<end,
361+
replacement: newNode.description
362+
)
363+
364+
case .replaceLeadingTrivia(let token, let newTrivia):
365+
let start = expansionContext.position(of: token.position, anchoredAt: token)
366+
let end = expansionContext.position(of: token.positionAfterSkippingLeadingTrivia, anchoredAt: token)
367+
return SourceEdit(
368+
range: start..<end,
369+
replacement: newTrivia.description
370+
)
371+
372+
case .replaceTrailingTrivia(let token, let newTrivia):
373+
let start = expansionContext.position(of: token.endPositionBeforeTrailingTrivia, anchoredAt: token)
374+
let end = expansionContext.position(of: token.endPosition, anchoredAt: token)
375+
return SourceEdit(
376+
range: start..<end,
377+
replacement: newTrivia.description
378+
)
379+
}
380+
}
381+
}
382+
383+
fileprivate extension BasicMacroExpansionContext {
384+
/// Translates a position from a detached node to the corresponding position
385+
/// in the original source file.
386+
func position(
387+
of position: AbsolutePosition,
388+
anchoredAt node: some SyntaxProtocol
389+
) -> AbsolutePosition {
390+
let location = self.location(for: position, anchoredAt: Syntax(node), fileName: "")
391+
return AbsolutePosition(utf8Offset: location.offset)
392+
}
393+
}

Sources/_SwiftSyntaxTestSupport/FixItApplier.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import SwiftDiagnostics
1414
import SwiftSyntax
15+
import SwiftSyntaxMacroExpansion
1516

1617
public enum FixItApplier {
1718
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
@@ -22,20 +23,34 @@ public enum FixItApplier {
2223
/// If `nil`, the first Fix-It from each diagnostic is applied.
2324
/// - tree: The syntax tree to which the Fix-Its will be applied.
2425
///
25-
/// - Returns: A ``String`` representation of the modified syntax tree after applying the Fix-Its.
26+
/// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
2627
public static func applyFixes(
2728
from diagnostics: [Diagnostic],
2829
filterByMessages messages: [String]?,
2930
to tree: any SyntaxProtocol
3031
) -> String {
3132
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
3233

33-
var edits =
34+
let edits =
3435
diagnostics
3536
.flatMap(\.fixIts)
3637
.filter { messages.contains($0.message.message) }
3738
.flatMap(\.edits)
3839

40+
return self.apply(edits: edits, to: tree)
41+
}
42+
43+
/// Apply the given edits to the syntax tree.
44+
///
45+
/// - Parameters:
46+
/// - edits: The edits to apply to the syntax tree
47+
/// - tree: he syntax tree to which the edits should be applied.
48+
/// - Returns: A `String` representation of the modified syntax tree after applying the edits.
49+
public static func apply(
50+
edits: [SourceEdit],
51+
to tree: any SyntaxProtocol
52+
) -> String {
53+
var edits = edits
3954
var source = tree.description
4055

4156
while let edit = edits.first {

Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,38 @@ final class PeerMacroTests: XCTestCase {
247247
indentationWidth: indentationWidth
248248
)
249249
}
250+
251+
func testAdjustFixItLocationsWhenComputingFixedSource() {
252+
// Test that we adjust the locations of the Fix-Its to the original source
253+
// before computing the `fixedSource` if the macro doesn't start at the
254+
// start of the file.
255+
assertMacroExpansion(
256+
"""
257+
func other() {}
258+
259+
@addCompletionHandler
260+
func f(a: Int, for b: String, _ value: Double) -> String { }
261+
""",
262+
expandedSource: """
263+
func other() {}
264+
func f(a: Int, for b: String, _ value: Double) -> String { }
265+
""",
266+
diagnostics: [
267+
DiagnosticSpec(
268+
message: "can only add a completion-handler variant to an 'async' function",
269+
line: 4,
270+
column: 1,
271+
fixIts: [FixItSpec(message: "add 'async'")]
272+
)
273+
],
274+
macros: ["addCompletionHandler": AddCompletionHandler.self],
275+
fixedSource: """
276+
func other() {}
277+
278+
@addCompletionHandler
279+
func f(a: Int, for b: String, _ value: Double) async-> String { }
280+
""",
281+
indentationWidth: indentationWidth
282+
)
283+
}
250284
}

0 commit comments

Comments
 (0)