Skip to content

Commit 4003c14

Browse files
committed
Introduce FixIt.Change.textualReplacement
Introduce a new case to FixIt.Change to express an unstructured edit, which replaces some range of source text (in a given file) with some other source text. This is needed for some edits that aren't easily mapped to the syntax tree, or when coming from other tools (such as the compiler) that don't express these fixes in terms of syntax in the first place.
1 parent 4ed73b1 commit 4003c14

File tree

4 files changed

+42
-0
lines changed

4 files changed

+42
-0
lines changed

Sources/SwiftCompilerPluginMessageHandling/Diagnostics.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ extension PluginMessage.Diagnostic {
150150
case .replaceChild(let replaceChildData):
151151
range = sourceManager.range(replaceChildData.replacementRange, in: replaceChildData.parent)
152152
text = replaceChildData.newChild.description
153+
case .textualReplacement(replacementRange: let replacementRange, sourceFile: let sourceFile, newText: let newText):
154+
range = sourceManager.range(replacementRange, in: sourceFile)
155+
text = newText
153156
#if RESILIENT_LIBRARIES
154157
@unknown default:
155158
fatalError()

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ public struct FixIt: Sendable {
7777
case replaceTrailingTrivia(token: TokenSyntax, newTrivia: Trivia)
7878
/// Replace the child node of the given parent node at the given replacement range with the given new child node
7979
case replaceChild(data: any ReplacingChildData)
80+
/// Replace the text within the given range in a source file with new text.
81+
///
82+
/// Generally, one should use other cases to replace specific syntax nodes
83+
/// or trivia, because it more easily leads to a correct result. However,
84+
/// this case provides a fallback for textual replacement that ignores
85+
/// syntactic structure. After applying a textual replacement, there is no
86+
/// way to get back to a syntax tree without reparsing.
87+
case textualReplacement(replacementRange: Range<AbsolutePosition>, sourceFile: SourceFileSyntax, newText: String)
8088
}
8189

8290
/// A description of what this Fix-It performs.
@@ -157,6 +165,9 @@ private extension FixIt.Change {
157165
range: replacingChildData.replacementRange,
158166
replacement: replacingChildData.newChild.description
159167
)
168+
169+
case .textualReplacement(replacementRange: let replacementRange, sourceFile: _, newText: let newText):
170+
return SourceEdit(range: replacementRange, replacement: newText)
160171
}
161172
}
162173
}

Sources/SwiftSyntaxMacrosGenericTestSupport/Assertions.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,11 @@ fileprivate extension FixIt.Change {
632632
range: start..<end,
633633
replacement: replacingChildData.newChild.description
634634
)
635+
636+
case .textualReplacement(replacementRange: let range, sourceFile: let sourceFile, newText: let newText):
637+
let start = expansionContext.position(of: range.lowerBound, anchoredAt: sourceFile)
638+
let end = expansionContext.position(of: range.upperBound, anchoredAt: sourceFile)
639+
return SourceEdit(range: start..<end, replacement: newText)
635640
}
636641
}
637642
}

Tests/SwiftDiagnosticsTest/FixItTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,21 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftDiagnostics
1314
import SwiftParser
1415
import SwiftParserDiagnostics
1516
import SwiftSyntax
1617
import XCTest
1718
import _SwiftSyntaxTestSupport
1819

20+
struct SimpleFixItMessage: FixItMessage {
21+
let message: String
22+
23+
var fixItID: MessageID {
24+
MessageID(domain: "here", id: "this")
25+
}
26+
}
27+
1928
final class FixItTests: XCTestCase {
2029
func testEditsForFixIt() throws {
2130
let markedSource = "protocol 1️⃣Multi2️⃣ 3️⃣ident 4️⃣{}"
@@ -39,4 +48,18 @@ final class FixItTests: XCTestCase {
3948
XCTAssertNotEqual(changes.count, edits.count)
4049
XCTAssertEqual(expectedEdits, edits)
4150
}
51+
52+
func testTextualReplacement() throws {
53+
let five = AbsolutePosition(utf8Offset: 5)
54+
let fifteen = AbsolutePosition(utf8Offset: 15)
55+
let change = FixIt(
56+
message: SimpleFixItMessage(message: "fix it please"),
57+
changes: [
58+
.textualReplacement(replacementRange: five..<fifteen, sourceFile: "func myFunction() { }", newText: "yours")
59+
]
60+
)
61+
XCTAssertEqual(change.edits.count, 1)
62+
XCTAssertEqual(change.edits[0].range, five..<fifteen)
63+
XCTAssertEqual(change.edits[0].replacement, "yours")
64+
}
4265
}

0 commit comments

Comments
 (0)