Skip to content

Adjust locations for edits based on detached nodes when computing fixedSource #2337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ let package = Package(

.target(
name: "_SwiftSyntaxTestSupport",
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder"]
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacroExpansion"]
),

.testTarget(
Expand Down
58 changes: 57 additions & 1 deletion Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,16 @@ public func assertMacroExpansion(

// Applying Fix-Its
if let expectedFixedSource = expectedFixedSource {
let fixedTree = FixItApplier.applyFixes(from: context.diagnostics, filterByMessages: applyFixIts, to: origSourceFile)
let messages = applyFixIts ?? context.diagnostics.compactMap { $0.fixIts.first?.message.message }

let edits =
context.diagnostics
.flatMap(\.fixIts)
.filter { messages.contains($0.message.message) }
.flatMap { $0.changes }
.map { $0.edit(in: context) }

let fixedTree = FixItApplier.apply(edits: edits, to: origSourceFile)
let fixedTreeDescription = fixedTree.description
assertStringsEqualWithDiff(
fixedTreeDescription.trimmingTrailingWhitespace(),
Expand All @@ -335,3 +344,50 @@ public func assertMacroExpansion(
)
}
}

fileprivate extension FixIt.Change {
/// Returns the edit for this change, translating positions from detached nodes
/// to the corresponding locations in the original source file based on
/// `expansionContext`.
///
/// - SeeAlso: `FixIt.Change.edit`
func edit(in expansionContext: BasicMacroExpansionContext) -> SourceEdit {
switch self {
case .replace(let oldNode, let newNode):
let start = expansionContext.position(of: oldNode.position, anchoredAt: oldNode)
let end = expansionContext.position(of: oldNode.endPosition, anchoredAt: oldNode)
return SourceEdit(
range: start..<end,
replacement: newNode.description
)

case .replaceLeadingTrivia(let token, let newTrivia):
let start = expansionContext.position(of: token.position, anchoredAt: token)
let end = expansionContext.position(of: token.positionAfterSkippingLeadingTrivia, anchoredAt: token)
return SourceEdit(
range: start..<end,
replacement: newTrivia.description
)

case .replaceTrailingTrivia(let token, let newTrivia):
let start = expansionContext.position(of: token.endPositionBeforeTrailingTrivia, anchoredAt: token)
let end = expansionContext.position(of: token.endPosition, anchoredAt: token)
return SourceEdit(
range: start..<end,
replacement: newTrivia.description
)
}
}
}

fileprivate extension BasicMacroExpansionContext {
/// Translates a position from a detached node to the corresponding position
/// in the original source file.
func position(
of position: AbsolutePosition,
anchoredAt node: some SyntaxProtocol
) -> AbsolutePosition {
let location = self.location(for: position, anchoredAt: Syntax(node), fileName: "")
return AbsolutePosition(utf8Offset: location.offset)
}
}
19 changes: 17 additions & 2 deletions Sources/_SwiftSyntaxTestSupport/FixItApplier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import SwiftDiagnostics
import SwiftSyntax
import SwiftSyntaxMacroExpansion

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

var edits =
let edits =
diagnostics
.flatMap(\.fixIts)
.filter { messages.contains($0.message.message) }
.flatMap(\.edits)

return self.apply(edits: edits, to: tree)
}

/// Apply the given edits to the syntax tree.
///
/// - Parameters:
/// - edits: The edits to apply to the syntax tree
/// - tree: he syntax tree to which the edits should be applied.
/// - Returns: A `String` representation of the modified syntax tree after applying the edits.
public static func apply(
edits: [SourceEdit],
to tree: any SyntaxProtocol
) -> String {
var edits = edits
var source = tree.description

while let edit = edits.first {
Expand Down
34 changes: 34 additions & 0 deletions Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,38 @@ final class PeerMacroTests: XCTestCase {
indentationWidth: indentationWidth
)
}

func testAdjustFixItLocationsWhenComputingFixedSource() {
// Test that we adjust the locations of the Fix-Its to the original source
// before computing the `fixedSource` if the macro doesn't start at the
// start of the file.
assertMacroExpansion(
"""
func other() {}

@addCompletionHandler
func f(a: Int, for b: String, _ value: Double) -> String { }
""",
expandedSource: """
func other() {}
func f(a: Int, for b: String, _ value: Double) -> String { }
""",
diagnostics: [
DiagnosticSpec(
message: "can only add a completion-handler variant to an 'async' function",
line: 4,
column: 1,
fixIts: [FixItSpec(message: "add 'async'")]
)
],
macros: ["addCompletionHandler": AddCompletionHandler.self],
fixedSource: """
func other() {}

@addCompletionHandler
func f(a: Int, for b: String, _ value: Double) async-> String { }
""",
indentationWidth: indentationWidth
)
}
}