Skip to content

Commit 9ea50a7

Browse files
committed
FixItApplier: Add parameter for skipping duplicate insertions
1 parent adf36e6 commit 9ea50a7

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
@@ -134,7 +134,10 @@ class FixItApplierApplyEditsTests: XCTestCase {
134134
.init(range: 3..<7, replacement: "cd"),
135135
],
136136
// The second edit is skipped.
137-
possibleOutputs: ["aboo = 1", "varcd = 1"]
137+
outputs: [
138+
.init("aboo = 1"),
139+
.init("varcd = 1"),
140+
]
138141
)
139142
}
140143

@@ -148,19 +151,37 @@ class FixItApplierApplyEditsTests: XCTestCase {
148151
.init(range: 0..<5, replacement: "_"),
149152
.init(range: 0..<3, replacement: "let"),
150153
],
151-
possibleOutputs: ["_ = 11", "let x = 11"]
154+
outputs: [
155+
.init("_ = 11"),
156+
.init("let x = 11"),
157+
]
152158
)
153159
}
154160

155161
func testMultipleOverlappingInsertions() {
162+
assertAppliedEdits(
163+
to: "x = 1",
164+
edits: [
165+
.init(range: 1..<1, replacement: "y"),
166+
.init(range: 1..<1, replacement: "z"),
167+
],
168+
outputs: [
169+
.init("xyz = 1"),
170+
.init("xzy = 1"),
171+
]
172+
)
173+
156174
assertAppliedEdits(
157175
to: "x = 1",
158176
edits: [
159177
.init(range: 0..<0, replacement: "var "),
160178
.init(range: 0..<0, replacement: "var "),
161179
.init(range: 0..<0, replacement: "var "),
162180
],
163-
output: "var var var x = 1"
181+
outputs: [
182+
.init("var var var x = 1", allowDuplicateInsertions: true),
183+
.init("var x = 1", allowDuplicateInsertions: false),
184+
]
164185
)
165186
}
166187

@@ -182,28 +203,69 @@ class FixItApplierApplyEditsTests: XCTestCase {
182203
.init(range: 2..<2, replacement: "a"), // Insertion
183204
],
184205
// FIXME: This behavior where these edits are not considered overlapping doesn't feel desirable
185-
possibleOutputs: ["_x = 1", "_ a= 1"]
206+
outputs: [
207+
.init("_x = 1"),
208+
.init("_ a= 1"),
209+
]
186210
)
187211
}
188212
}
189213

214+
private struct Output {
215+
var result: String
216+
var allowDuplicateInsertions: Bool?
217+
218+
init(_ result: String, allowDuplicateInsertions: Bool? = nil) {
219+
self.result = result
220+
self.allowDuplicateInsertions = allowDuplicateInsertions
221+
}
222+
}
223+
190224
/// Asserts that at least one element in `possibleOutputs` matches the result
191225
/// of applying an array of edits to `input`, for all permutations of `edits`.
192226
private func assertAppliedEdits(
193227
to tree: SourceFileSyntax,
194228
edits: [SourceEdit],
195-
possibleOutputs: [String]
229+
outputs: [Output]
196230
) {
197-
precondition(!possibleOutputs.isEmpty)
231+
precondition(!outputs.isEmpty)
198232

199-
var indices = Array(edits.indices)
200-
while true {
201-
let result = FixItApplier.apply(edits: indices.map { edits[$0] }, to: tree)
202-
guard possibleOutputs.contains(result) else {
203-
XCTFail("\"\(result)\" is not equal to either of \(possibleOutputs)")
233+
func assertAppliedEdits(
234+
permutation: [SourceEdit],
235+
allowDuplicateInsertions: Bool
236+
) {
237+
// Filter out the results that match this setting.
238+
let viableResults: [String] = outputs.compactMap { output in
239+
if output.allowDuplicateInsertions == !allowDuplicateInsertions {
240+
return nil
241+
}
242+
243+
return output.result
244+
}
245+
246+
guard !viableResults.isEmpty else {
204247
return
205248
}
206249

250+
let result = FixItApplier.apply(
251+
edits: permutation,
252+
to: tree,
253+
allowDuplicateInsertions: allowDuplicateInsertions
254+
)
255+
256+
guard viableResults.contains(result) else {
257+
XCTFail("\"\(result)\" is not equal to either of \(viableResults)")
258+
return
259+
}
260+
}
261+
262+
var indices = Array(edits.indices)
263+
while true {
264+
let permutation = indices.map { edits[$0] }
265+
266+
assertAppliedEdits(permutation: permutation, allowDuplicateInsertions: true)
267+
assertAppliedEdits(permutation: permutation, allowDuplicateInsertions: false)
268+
207269
let keepGoing = indices.nextPermutation()
208270
guard keepGoing else {
209271
break
@@ -218,7 +280,13 @@ private func assertAppliedEdits(
218280
edits: [SourceEdit],
219281
output: String
220282
) {
221-
assertAppliedEdits(to: tree, edits: edits, possibleOutputs: [output])
283+
assertAppliedEdits(
284+
to: tree,
285+
edits: edits,
286+
outputs: [
287+
.init(output)
288+
]
289+
)
222290
}
223291

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

0 commit comments

Comments
 (0)