Skip to content

Commit 3a2b93a

Browse files
committed
FixItApplier: Add parameter for skipping duplicate insertions
1 parent adf36e6 commit 3a2b93a

File tree

2 files changed

+132
-28
lines changed

2 files changed

+132
-28
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: 107 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ 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+
.either(["aboo = 1", "varcd = 1"])
139+
]
138140
)
139141
}
140142

@@ -148,19 +150,36 @@ class FixItApplierApplyEditsTests: XCTestCase {
148150
.init(range: 0..<5, replacement: "_"),
149151
.init(range: 0..<3, replacement: "let"),
150152
],
151-
possibleOutputs: ["_ = 11", "let x = 11"]
153+
outputs: [
154+
.either(["_ = 11", "let x = 11"])
155+
]
152156
)
153157
}
154158

155159
func testMultipleOverlappingInsertions() {
160+
assertAppliedEdits(
161+
to: "x = 1",
162+
edits: [
163+
.init(range: 1..<1, replacement: "y"),
164+
.init(range: 1..<1, replacement: "z"),
165+
],
166+
outputs: [
167+
.either(["xyz = 1", "xzy = 1"])
168+
]
169+
)
170+
156171
assertAppliedEdits(
157172
to: "x = 1",
158173
edits: [
159174
.init(range: 0..<0, replacement: "var "),
160175
.init(range: 0..<0, replacement: "var "),
176+
.init(range: 4..<5, replacement: "2"),
161177
.init(range: 0..<0, replacement: "var "),
162178
],
163-
output: "var var var x = 1"
179+
outputs: [
180+
.output("var var var x = 2", allowDuplicateInsertions: true),
181+
.output("var x = 2", allowDuplicateInsertions: false),
182+
]
164183
)
165184
}
166185

@@ -182,43 +201,110 @@ class FixItApplierApplyEditsTests: XCTestCase {
182201
.init(range: 2..<2, replacement: "a"), // Insertion
183202
],
184203
// FIXME: This behavior where these edits are not considered overlapping doesn't feel desirable
185-
possibleOutputs: ["_x = 1", "_ a= 1"]
204+
outputs: [
205+
.either(["_x = 1", "_ a= 1"])
206+
]
186207
)
187208
}
188209
}
189210

190-
/// Asserts that at least one element in `possibleOutputs` matches the result
191-
/// of applying an array of edits to `input`, for all permutations of `edits`.
211+
private enum Output {
212+
case output(String, allowDuplicateInsertions: Bool? = nil)
213+
case either([String], allowDuplicateInsertions: Bool? = nil)
214+
215+
var possibleOutputs: [String] {
216+
switch self {
217+
case .output(let output, _):
218+
return [output]
219+
case .either(let outputs, _):
220+
return outputs
221+
}
222+
}
223+
224+
var allowDuplicateInsertions: Bool? {
225+
switch self {
226+
case .output(_, let allowDuplicateInsertions),
227+
.either(_, let allowDuplicateInsertions):
228+
return allowDuplicateInsertions
229+
}
230+
}
231+
}
232+
233+
/// Asserts that the given outputs match the result of applying an array of
234+
/// edits to `input`, for all permutations of `edits`.
192235
private func assertAppliedEdits(
193236
to tree: SourceFileSyntax,
194237
edits: [SourceEdit],
195-
possibleOutputs: [String]
238+
outputs: [Output]
196239
) {
197-
precondition(!possibleOutputs.isEmpty)
198-
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)")
204-
return
205-
}
240+
precondition(!outputs.isEmpty)
241+
242+
NEXT_OUTPUT: for output in outputs {
243+
let allowDuplicateInsertionsValues =
244+
switch output.allowDuplicateInsertions {
245+
case .none:
246+
[true, false]
247+
case .some(let value):
248+
[value]
249+
}
250+
251+
let possibleOutputs = output.possibleOutputs
252+
253+
// Check this output against all permutations of edits.
254+
var indices = Array(edits.indices)
255+
while true {
256+
let editsPermutation = indices.map { edits[$0] }
257+
258+
for allowDuplicateInsertionsValue in allowDuplicateInsertionsValues {
259+
let actualOutput = FixItApplier.apply(
260+
edits: editsPermutation,
261+
to: tree,
262+
allowDuplicateInsertions: allowDuplicateInsertionsValue
263+
)
206264

207-
let keepGoing = indices.nextPermutation()
208-
guard keepGoing else {
209-
break
265+
guard possibleOutputs.contains(actualOutput) else {
266+
XCTFail(
267+
"""
268+
269+
Actual output:
270+
\"\(actualOutput)\"
271+
Expected output:
272+
either of \(possibleOutputs)
273+
Edits:
274+
\(editsPermutation)
275+
allowDuplicateInsertions:
276+
\(allowDuplicateInsertionsValue)
277+
"""
278+
)
279+
280+
// Fail once for all permutations to avoid excessive logging.
281+
continue NEXT_OUTPUT
282+
}
283+
}
284+
285+
let keepGoing = indices.nextPermutation()
286+
guard keepGoing else {
287+
break
288+
}
210289
}
211290
}
212291
}
213292

214293
/// Asserts that `output` matches the result of applying an array of edits to
215-
/// `input`, for all permutations of `edits`.
294+
/// `input`, for all permutations of `edits` and for `allowDuplicateInsertions`
295+
/// both `true` and `false`.
216296
private func assertAppliedEdits(
217297
to tree: SourceFileSyntax,
218298
edits: [SourceEdit],
219299
output: String
220300
) {
221-
assertAppliedEdits(to: tree, edits: edits, possibleOutputs: [output])
301+
assertAppliedEdits(
302+
to: tree,
303+
edits: edits,
304+
outputs: [
305+
.output(output, allowDuplicateInsertions: nil)
306+
]
307+
)
222308
}
223309

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

0 commit comments

Comments
 (0)