Skip to content
Open
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
30 changes: 28 additions & 2 deletions Sources/SwiftIDEUtils/FixItApplier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public import SwiftSyntax
public enum FixItApplier {
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
///
/// Edits from Fix-Its must use offsets from `tree` or the syntax tree that contains `tree`.
/// Individual edits that fall outside `tree` are ignored.
///
/// - Parameters:
/// - diagnostics: An array of `Diagnostic` objects, each containing one or more Fix-Its.
/// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
Expand All @@ -34,7 +37,6 @@ public enum FixItApplier {
) -> String {
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }

// FIXME: This assumes every fix-it is applied to a node in the 'tree', which is not guaranteed.
let edits =
diagnostics
.flatMap(\.fixIts)
Expand All @@ -46,6 +48,10 @@ public enum FixItApplier {

/// Applies the given edits to the given syntax tree.
///
/// Edit offsets must use the coordinate space of `tree` or the syntax tree that contains `tree`.
/// The applier adjusts these offsets to the range of `tree` and ignores edits
/// that fall outside `tree`.
///
/// - Parameters:
/// - edits: The edits to apply.
/// - tree: The syntax tree to which the edits should be applied.
Expand All @@ -58,8 +64,28 @@ public enum FixItApplier {
to tree: some SyntaxProtocol,
allowDuplicateInsertions: Bool = true
) -> String {
var edits = edits
var source = tree.description
let treeStartOffset = tree.position.utf8Offset
let sourceByteCount = source.utf8.count

var edits = edits.compactMap { (edit) -> SourceEdit? in
guard !edit.isEmpty else {
return nil
}

let relativeStart = edit.startUtf8Offset - treeStartOffset
let relativeEnd = edit.endUtf8Offset - treeStartOffset

// Filter out edits that do not fall within the subtree.
guard relativeStart >= 0, relativeEnd <= sourceByteCount, relativeStart <= relativeEnd else {
return nil
}

return SourceEdit(
range: AbsolutePosition(utf8Offset: relativeStart)..<AbsolutePosition(utf8Offset: relativeEnd),
replacement: edit.replacementBytes
)
}

for var editIndex in edits.indices {
let edit = edits[editIndex]
Expand Down
61 changes: 61 additions & 0 deletions Tests/SwiftIDEUtilsTest/FixItApplierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

@_spi(FixItApplier) import SwiftIDEUtils
import SwiftParser
import SwiftSyntax
import SwiftSyntaxBuilder
import XCTest
Expand Down Expand Up @@ -238,6 +239,66 @@ class FixItApplierApplyEditsTests: XCTestCase {
]
)
}

func testSubtreeEditInWholeFileCoordinates() {
let source = """
func test() {
var x = 1
let y = 2
}
"""
let file = Parser.parse(source: source)
let fn = Array(file.statements)[0].item.as(FunctionDeclSyntax.self)!
let stmt = Array(fn.body!.statements)[1]
XCTAssertGreaterThan(stmt.position.utf8Offset, 0)

let yDecl = stmt.item.as(VariableDeclSyntax.self)!
let bindingSpec = yDecl.bindingSpecifier
let edit = SourceEdit(
range: bindingSpec.position..<bindingSpec.endPosition,
replacement: " var "
)

let result = FixItApplier.apply(edits: [edit], to: stmt)
XCTAssertEqual(result, " var y = 2")
}

func testSubtreeIgnoresExternalEdits() {
let source = """
func test() {
var x = 1
let y = 2
var z = 3
}
"""
let file = Parser.parse(source: source)
let fn = Array(file.statements)[0].item.as(FunctionDeclSyntax.self)!
let firstStmt = Array(fn.body!.statements)[0]
let secondStmt = Array(fn.body!.statements)[1]
let thirdStmt = Array(fn.body!.statements)[2]

let xDecl = firstStmt.item.as(VariableDeclSyntax.self)!
let editBefore = SourceEdit(
range: xDecl.bindingSpecifier.position..<xDecl.bindingSpecifier.endPosition,
replacement: " let "
)

let zDecl = thirdStmt.item.as(VariableDeclSyntax.self)!
let editAfter = SourceEdit(
range: zDecl.bindingSpecifier.position..<zDecl.bindingSpecifier.endPosition,
replacement: " let "
)

let yDecl = secondStmt.item.as(VariableDeclSyntax.self)!
let editInside = SourceEdit(
range: yDecl.bindingSpecifier.position..<yDecl.bindingSpecifier.endPosition,
replacement: " var "
)

// Apply edits before, inside, and after secondStmt. External edits must be ignored.
let result = FixItApplier.apply(edits: [editBefore, editInside, editAfter], to: secondStmt)
XCTAssertEqual(result, " var y = 2")
}
}

/// Represents an output expectation.
Expand Down