Skip to content

Commit c8d5641

Browse files
committed
FixItApplier: Add parameter for skipping duplicate insertions
1 parent 14aa20b commit c8d5641

File tree

2 files changed

+105
-19
lines changed

2 files changed

+105
-19
lines changed

Sources/SwiftIDEUtils/FixItApplier.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ public enum FixItApplier {
2727
/// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
2828
/// If `nil`, the first Fix-It from each diagnostic is applied.
2929
/// - tree: The syntax tree to which the Fix-Its will be applied.
30+
/// - allowDuplicateInsertions: Whether to apply duplicate insertions.
31+
/// Default to `true`.
3032
///
3133
/// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
3234
public static func applyFixes(
3335
from diagnostics: [Diagnostic],
3436
filterByMessages messages: [String]?,
35-
to tree: some SyntaxProtocol
37+
to tree: some SyntaxProtocol,
38+
allowDuplicateInsertions: Bool = true
3639
) -> String {
3740
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
3841

@@ -43,18 +46,22 @@ public enum FixItApplier {
4346
.filter { messages.contains($0.message.message) }
4447
.flatMap(\.edits)
4548

46-
return self.apply(edits: edits, to: tree)
49+
return self.apply(edits: edits, to: tree, allowDuplicateInsertions: allowDuplicateInsertions)
4750
}
4851

4952
/// Applies the given edits to the given syntax tree.
5053
///
5154
/// - Parameters:
5255
/// - edits: The edits to apply.
5356
/// - tree: The syntax tree to which the edits should be applied.
57+
/// - allowDuplicateInsertions: Whether to apply duplicate insertions.
58+
/// Default to `true`.
59+
///
5460
/// - Returns: A `String` representation of the modified syntax tree.
5561
public static func apply(
5662
edits: [SourceEdit],
57-
to tree: some SyntaxProtocol
63+
to tree: some SyntaxProtocol,
64+
allowDuplicateInsertions: Bool = true
5865
) -> String {
5966
var edits = edits
6067
var source = tree.description
@@ -89,10 +96,21 @@ public enum FixItApplier {
8996
continue
9097
}
9198

92-
guard !remainingEdit.range.overlaps(edit.range) else {
93-
// The edit overlaps with the previous edit. We can't apply both
94-
// without conflicts. Drop this one by swapping it for a no-op
95-
// edit.
99+
func shouldDropRemainingEdit() -> Bool {
100+
// Insertions never conflict between themselves, unless we were asked
101+
// to drop duplicate insertions.
102+
if edit.range.isEmpty && remainingEdit.range.isEmpty {
103+
guard allowDuplicateInsertions else {
104+
return edit == remainingEdit
105+
}
106+
return false
107+
}
108+
109+
return remainingEdit.range.overlaps(edit.range)
110+
}
111+
112+
guard !shouldDropRemainingEdit() else {
113+
// Drop the edit by swapping it for an empty one.
96114
edits[editIndex] = SourceEdit()
97115
continue
98116
}

Tests/SwiftIDEUtilsTest/FixItApplierTests.swift

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ class FixItApplierApplyEditsTests: XCTestCase {
138138
.init(range: 3..<7, replacement: "cd"),
139139
],
140140
// The second edit is skipped.
141-
possibleOutputs: ["aboo = 1", "varcd = 1"]
141+
outputs: [
142+
.init("aboo = 1"),
143+
.init("varcd = 1"),
144+
]
142145
)
143146
}
144147

@@ -152,19 +155,37 @@ class FixItApplierApplyEditsTests: XCTestCase {
152155
.init(range: 0..<5, replacement: "_"),
153156
.init(range: 0..<3, replacement: "let"),
154157
],
155-
possibleOutputs: ["_ = 11", "let x = 11"]
158+
outputs: [
159+
.init("_ = 11"),
160+
.init("let x = 11"),
161+
]
156162
)
157163
}
158164

159165
func testMultipleOverlappingInsertions() {
166+
assertAppliedEdits(
167+
to: "x = 1",
168+
edits: [
169+
.init(range: 1..<1, replacement: "y"),
170+
.init(range: 1..<1, replacement: "z"),
171+
],
172+
outputs: [
173+
.init("xyz = 1"),
174+
.init("xzy = 1"),
175+
]
176+
)
177+
160178
assertAppliedEdits(
161179
to: "x = 1",
162180
edits: [
163181
.init(range: 0..<0, replacement: "var "),
164182
.init(range: 0..<0, replacement: "var "),
165183
.init(range: 0..<0, replacement: "var "),
166184
],
167-
output: "var var var x = 1"
185+
outputs: [
186+
.init("var var var x = 1", allowDuplicateInsertions: true),
187+
.init("var x = 1", allowDuplicateInsertions: false),
188+
]
168189
)
169190
}
170191

@@ -186,28 +207,69 @@ class FixItApplierApplyEditsTests: XCTestCase {
186207
.init(range: 2..<2, replacement: "a"), // Insertion
187208
],
188209
// FIXME: This behavior where these edits are not considered overlapping doesn't feel desirable
189-
possibleOutputs: ["_x = 1", "_ a= 1"]
210+
outputs: [
211+
.init("_x = 1"),
212+
.init("_ a= 1"),
213+
]
190214
)
191215
}
192216
}
193217

218+
private struct Output {
219+
var result: String
220+
var allowDuplicateInsertions: Bool?
221+
222+
init(_ result: String, allowDuplicateInsertions: Bool? = nil) {
223+
self.result = result
224+
self.allowDuplicateInsertions = allowDuplicateInsertions
225+
}
226+
}
227+
194228
/// Asserts that at least one element in `possibleOutputs` matches the result
195229
/// of applying an array of edits to `input`, for all permutations of `edits`.
196230
private func assertAppliedEdits(
197231
to tree: SourceFileSyntax,
198232
edits: [SourceEdit],
199-
possibleOutputs: [String]
233+
outputs: [Output]
200234
) {
201-
precondition(!possibleOutputs.isEmpty)
235+
precondition(!outputs.isEmpty)
202236

203-
var indices = Array(edits.indices)
204-
while true {
205-
let result = FixItApplier.apply(edits: indices.map { edits[$0] }, to: tree)
206-
guard possibleOutputs.contains(result) else {
207-
XCTFail("\"\(result)\" is not equal to either of \(possibleOutputs)")
237+
func assertAppliedEdits(
238+
permutation: [SourceEdit],
239+
allowDuplicateInsertions: Bool
240+
) {
241+
// Filter out the results that match this setting.
242+
let viableResults: [String] = outputs.compactMap { output in
243+
if output.allowDuplicateInsertions == !allowDuplicateInsertions {
244+
return nil
245+
}
246+
247+
return output.result
248+
}
249+
250+
guard !viableResults.isEmpty else {
208251
return
209252
}
210253

254+
let result = FixItApplier.apply(
255+
edits: permutation,
256+
to: tree,
257+
allowDuplicateInsertions: allowDuplicateInsertions
258+
)
259+
260+
guard viableResults.contains(result) else {
261+
XCTFail("\"\(result)\" is not equal to either of \(viableResults)")
262+
return
263+
}
264+
}
265+
266+
var indices = Array(edits.indices)
267+
while true {
268+
let permutation = indices.map { edits[$0] }
269+
270+
assertAppliedEdits(permutation: permutation, allowDuplicateInsertions: true)
271+
assertAppliedEdits(permutation: permutation, allowDuplicateInsertions: false)
272+
211273
let keepGoing = indices.nextPermutation()
212274
guard keepGoing else {
213275
break
@@ -222,7 +284,13 @@ private func assertAppliedEdits(
222284
edits: [SourceEdit],
223285
output: String
224286
) {
225-
assertAppliedEdits(to: tree, edits: edits, possibleOutputs: [output])
287+
assertAppliedEdits(
288+
to: tree,
289+
edits: edits,
290+
outputs: [
291+
.init(output)
292+
]
293+
)
226294
}
227295

228296
// Grabbed from https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Permutations.swift

0 commit comments

Comments
 (0)