-
Notifications
You must be signed in to change notification settings - Fork 441
/
Copy pathFixItApplier.swift
110 lines (95 loc) · 3.88 KB
/
FixItApplier.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if compiler(>=6)
public import SwiftDiagnostics
public import SwiftSyntax
#else
import SwiftDiagnostics
import SwiftSyntax
#endif
@_spi(FixItApplier)
public enum FixItApplier {
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
///
/// - 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.
/// If `nil`, the first Fix-It from each diagnostic is applied.
/// - tree: The syntax tree to which the Fix-Its will be applied.
///
/// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
public static func applyFixes(
from diagnostics: [Diagnostic],
filterByMessages messages: [String]?,
to tree: any SyntaxProtocol
) -> 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)
.filter { messages.contains($0.message.message) }
.flatMap(\.edits)
return self.apply(edits: edits, to: tree)
}
/// Apply the given edits to the syntax tree.
///
/// - Parameters:
/// - edits: The edits to apply to the syntax tree
/// - tree: he syntax tree to which the edits should be applied.
/// - Returns: A `String` representation of the modified syntax tree after applying the edits.
public static func apply(
edits: [SourceEdit],
to tree: any SyntaxProtocol
) -> String {
var edits = edits
var source = tree.description
while let edit = edits.first {
edits = Array(edits.dropFirst())
let startIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.startUtf8Offset)
let endIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.endUtf8Offset)
source.replaceSubrange(startIndex..<endIndex, with: edit.replacement)
edits = edits.compactMap { remainingEdit -> SourceEdit? in
if remainingEdit.replacementRange.overlaps(edit.replacementRange) {
// The edit overlaps with the previous edit. We can't apply both
// without conflicts. Apply the one that's listed first and drop the
// later edit.
return nil
}
// If the remaining edit starts after or at the end of the edit that we just applied,
// shift it by the current edit's difference in length.
if edit.endUtf8Offset <= remainingEdit.startUtf8Offset {
let startPosition = AbsolutePosition(
utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength.utf8Length
)
let endPosition = AbsolutePosition(
utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength.utf8Length
)
return SourceEdit(range: startPosition..<endPosition, replacement: remainingEdit.replacement)
}
return remainingEdit
}
}
return source
}
}
private extension SourceEdit {
var startUtf8Offset: Int {
return range.lowerBound.utf8Offset
}
var endUtf8Offset: Int {
return range.upperBound.utf8Offset
}
var replacementRange: Range<Int> {
return startUtf8Offset..<endUtf8Offset
}
}