Skip to content

Commit d9ae448

Browse files
authored
Merge pull request #2337 from ahoppen/ahoppen/fix-it-location-translations
Adjust locations for edits based on detached nodes when computing `fixedSource`
2 parents 0e6e974 + 1bd7cd4 commit d9ae448

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)