Skip to content

Commit 0747547

Browse files
authored
Merge pull request #2314 from joehsieh/add-fix-it-extesion
Add edits to FixIt for sourcekit-lsp to access
2 parents db0aad7 + 1d84c20 commit 0747547

File tree

7 files changed

+185
-105
lines changed

7 files changed

+185
-105
lines changed

Release Notes/511.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Swift Syntax 511 Release Notes
22

33
## New APIs
4+
- FixIt now has a new computed propery named edits
5+
- Description: the edits represent the non-overlapping textual edits that need to be performed when the Fix-It is applied.
6+
- Issue: https://github.com/apple/sourcekit-lsp/issues/909
7+
- Pull Request: https://github.com/apple/swift-syntax/pull/2314
8+
9+
- SourceEdit
10+
- Description: SourceEdit has been moved from SwiftRefactor to SwiftSyntax
11+
- Issue: https://github.com/apple/sourcekit-lsp/issues/909
12+
- Pull Request: https://github.com/apple/swift-syntax/pull/2314
413

514
- `assertMacroExpansion` now have new parameters named `applyFixIts` and `fixedSource`
615
- Description: `applyFixIts` and `fixedSource` are used to assert so ensure that the source code after applying Fix-Its matches this string.

Sources/SwiftDiagnostics/FixIt.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,45 @@ public struct FixIt {
4646
self.changes = changes
4747
}
4848
}
49+
50+
extension FixIt {
51+
/// The edits represent the non-overlapping textual edits that need to be performed when the Fix-It is applied.
52+
public var edits: [SourceEdit] {
53+
var existingEdits = [SourceEdit]()
54+
for change in changes {
55+
let edit = change.edit
56+
let isOverlapping = existingEdits.contains { edit.range.overlaps($0.range) }
57+
if !isOverlapping {
58+
// The edit overlaps with the previous edit. We can't apply both
59+
// without conflicts. Apply the one that's listed first and drop the
60+
// later edit.
61+
existingEdits.append(edit)
62+
}
63+
}
64+
return existingEdits
65+
}
66+
}
67+
68+
private extension FixIt.Change {
69+
var edit: SourceEdit {
70+
switch self {
71+
case .replace(let oldNode, let newNode):
72+
return SourceEdit(
73+
range: oldNode.position..<oldNode.endPosition,
74+
replacement: newNode.description
75+
)
76+
77+
case .replaceLeadingTrivia(let token, let newTrivia):
78+
return SourceEdit(
79+
range: token.position..<token.positionAfterSkippingLeadingTrivia,
80+
replacement: newTrivia.description
81+
)
82+
83+
case .replaceTrailingTrivia(let token, let newTrivia):
84+
return SourceEdit(
85+
range: token.endPositionBeforeTrailingTrivia..<token.endPosition,
86+
replacement: newTrivia.description
87+
)
88+
}
89+
}
90+
}

Sources/SwiftRefactor/RefactoringProvider.swift

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -109,64 +109,3 @@ extension SyntaxRefactoringProvider {
109109
return [SourceEdit.replace(syntax, with: output.description)]
110110
}
111111
}
112-
113-
/// A textual edit to the original source represented by a range and a
114-
/// replacement.
115-
public struct SourceEdit: Equatable {
116-
/// The half-open range that this edit applies to.
117-
public let range: Range<AbsolutePosition>
118-
/// The text to replace the original range with. Empty for a deletion.
119-
public let replacement: String
120-
121-
/// Length of the original source range that this edit applies to. Zero if
122-
/// this is an addition.
123-
public var length: SourceLength {
124-
return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
125-
}
126-
127-
/// Create an edit to replace `range` in the original source with
128-
/// `replacement`.
129-
public init(range: Range<AbsolutePosition>, replacement: String) {
130-
self.range = range
131-
self.replacement = replacement
132-
}
133-
134-
/// Convenience function to create a textual addition after the given node
135-
/// and its trivia.
136-
public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
137-
return SourceEdit(range: node.endPosition..<node.endPosition, replacement: newText)
138-
}
139-
140-
/// Convenience function to create a textual addition before the given node
141-
/// and its trivia.
142-
public static func insert(_ newText: String, before node: some SyntaxProtocol) -> SourceEdit {
143-
return SourceEdit(range: node.position..<node.position, replacement: newText)
144-
}
145-
146-
/// Convenience function to create a textual replacement of the given node,
147-
/// including its trivia.
148-
public static func replace(_ node: some SyntaxProtocol, with replacement: String) -> SourceEdit {
149-
return SourceEdit(range: node.position..<node.endPosition, replacement: replacement)
150-
}
151-
152-
/// Convenience function to create a textual deletion the given node and its
153-
/// trivia.
154-
public static func remove(_ node: some SyntaxProtocol) -> SourceEdit {
155-
return SourceEdit(range: node.position..<node.endPosition, replacement: "")
156-
}
157-
}
158-
159-
extension SourceEdit: CustomDebugStringConvertible {
160-
public var debugDescription: String {
161-
let hasNewline = replacement.contains { $0.isNewline }
162-
if hasNewline {
163-
return #"""
164-
\#(range.lowerBound.utf8Offset)-\#(range.upperBound.utf8Offset)
165-
"""
166-
\#(replacement)
167-
"""
168-
"""#
169-
}
170-
return "\(range.lowerBound.utf8Offset)-\(range.upperBound.utf8Offset) \"\(replacement)\""
171-
}
172-
}

Sources/SwiftSyntax/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ add_swift_syntax_library(SwiftSyntax
1616
Convenience.swift
1717
MemoryLayout.swift
1818
MissingNodeInitializers.swift
19+
SourceEdit.swift
1920
SourceLength.swift
2021
SourceLocation.swift
2122
SourcePresence.swift

Sources/SwiftSyntax/SourceEdit.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A textual edit to the original source represented by a range and a
14+
/// replacement.
15+
public struct SourceEdit: Equatable {
16+
/// The half-open range that this edit applies to.
17+
public let range: Range<AbsolutePosition>
18+
/// The text to replace the original range with. Empty for a deletion.
19+
public let replacement: String
20+
21+
/// Length of the original source range that this edit applies to. Zero if
22+
/// this is an addition.
23+
public var length: SourceLength {
24+
return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
25+
}
26+
27+
/// Create an edit to replace `range` in the original source with
28+
/// `replacement`.
29+
public init(range: Range<AbsolutePosition>, replacement: String) {
30+
self.range = range
31+
self.replacement = replacement
32+
}
33+
34+
/// Convenience function to create a textual addition after the given node
35+
/// and its trivia.
36+
public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
37+
return SourceEdit(range: node.endPosition..<node.endPosition, replacement: newText)
38+
}
39+
40+
/// Convenience function to create a textual addition before the given node
41+
/// and its trivia.
42+
public static func insert(_ newText: String, before node: some SyntaxProtocol) -> SourceEdit {
43+
return SourceEdit(range: node.position..<node.position, replacement: newText)
44+
}
45+
46+
/// Convenience function to create a textual replacement of the given node,
47+
/// including its trivia.
48+
public static func replace(_ node: some SyntaxProtocol, with replacement: String) -> SourceEdit {
49+
return SourceEdit(range: node.position..<node.endPosition, replacement: replacement)
50+
}
51+
52+
/// Convenience function to create a textual deletion the given node and its
53+
/// trivia.
54+
public static func remove(_ node: some SyntaxProtocol) -> SourceEdit {
55+
return SourceEdit(range: node.position..<node.endPosition, replacement: "")
56+
}
57+
}
58+
59+
extension SourceEdit: CustomDebugStringConvertible {
60+
public var debugDescription: String {
61+
let hasNewline = replacement.contains { $0.isNewline }
62+
if hasNewline {
63+
return #"""
64+
\#(range.lowerBound.utf8Offset)-\#(range.upperBound.utf8Offset)
65+
"""
66+
\#(replacement)
67+
"""
68+
"""#
69+
}
70+
return "\(range.lowerBound.utf8Offset)-\(range.upperBound.utf8Offset) \"\(replacement)\""
71+
}
72+
}

Sources/_SwiftSyntaxTestSupport/FixItApplier.swift

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,6 @@ import SwiftDiagnostics
1414
import SwiftSyntax
1515

1616
public enum FixItApplier {
17-
struct Edit: Equatable {
18-
var startUtf8Offset: Int
19-
var endUtf8Offset: Int
20-
let replacement: String
21-
22-
var replacementLength: Int {
23-
return replacement.utf8.count
24-
}
25-
26-
var replacementRange: Range<Int> {
27-
return startUtf8Offset..<endUtf8Offset
28-
}
29-
}
30-
3117
/// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
3218
///
3319
/// - Parameters:
@@ -44,13 +30,12 @@ public enum FixItApplier {
4430
) -> String {
4531
let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
4632

47-
let changes =
33+
var edits =
4834
diagnostics
4935
.flatMap(\.fixIts)
5036
.filter { messages.contains($0.message.message) }
51-
.flatMap(\.changes)
37+
.flatMap(\.edits)
5238

53-
var edits: [Edit] = changes.map(\.edit)
5439
var source = tree.description
5540

5641
while let edit = edits.first {
@@ -61,9 +46,7 @@ public enum FixItApplier {
6146

6247
source.replaceSubrange(startIndex..<endIndex, with: edit.replacement)
6348

64-
edits = edits.compactMap { remainingEdit -> FixItApplier.Edit? in
65-
var remainingEdit = remainingEdit
66-
49+
edits = edits.compactMap { remainingEdit -> SourceEdit? in
6750
if remainingEdit.replacementRange.overlaps(edit.replacementRange) {
6851
// The edit overlaps with the previous edit. We can't apply both
6952
// without conflicts. Apply the one that's listed first and drop the
@@ -74,8 +57,9 @@ public enum FixItApplier {
7457
// If the remaining edit starts after or at the end of the edit that we just applied,
7558
// shift it by the current edit's difference in length.
7659
if edit.endUtf8Offset <= remainingEdit.startUtf8Offset {
77-
remainingEdit.startUtf8Offset = remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength
78-
remainingEdit.endUtf8Offset = remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength
60+
let startPosition = AbsolutePosition(utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count + edit.replacementLength)
61+
let endPosition = AbsolutePosition(utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count + edit.replacementLength)
62+
return SourceEdit(range: startPosition..<endPosition, replacement: remainingEdit.replacement)
7963
}
8064

8165
return remainingEdit
@@ -86,29 +70,20 @@ public enum FixItApplier {
8670
}
8771
}
8872

89-
fileprivate extension FixIt.Change {
90-
var edit: FixItApplier.Edit {
91-
switch self {
92-
case .replace(let oldNode, let newNode):
93-
return FixItApplier.Edit(
94-
startUtf8Offset: oldNode.position.utf8Offset,
95-
endUtf8Offset: oldNode.endPosition.utf8Offset,
96-
replacement: newNode.description
97-
)
73+
private extension SourceEdit {
74+
var startUtf8Offset: Int {
75+
return range.lowerBound.utf8Offset
76+
}
77+
78+
var endUtf8Offset: Int {
79+
return range.upperBound.utf8Offset
80+
}
9881

99-
case .replaceLeadingTrivia(let token, let newTrivia):
100-
return FixItApplier.Edit(
101-
startUtf8Offset: token.position.utf8Offset,
102-
endUtf8Offset: token.positionAfterSkippingLeadingTrivia.utf8Offset,
103-
replacement: newTrivia.description
104-
)
82+
var replacementLength: Int {
83+
return replacement.utf8.count
84+
}
10585

106-
case .replaceTrailingTrivia(let token, let newTrivia):
107-
return FixItApplier.Edit(
108-
startUtf8Offset: token.endPositionBeforeTrailingTrivia.utf8Offset,
109-
endUtf8Offset: token.endPosition.utf8Offset,
110-
replacement: newTrivia.description
111-
)
112-
}
86+
var replacementRange: Range<Int> {
87+
return startUtf8Offset..<endUtf8Offset
11388
}
11489
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftParser
14+
import SwiftParserDiagnostics
15+
import SwiftSyntax
16+
import XCTest
17+
import _SwiftSyntaxTestSupport
18+
19+
final class FixItTests: XCTestCase {
20+
func testEditsForFixIt() throws {
21+
let markedSource = "protocol 1️⃣Multi 2️⃣ident 3️⃣{}"
22+
let (markers, source) = extractMarkers(markedSource)
23+
let positions = markers.mapValues { AbsolutePosition(utf8Offset: $0) }
24+
XCTAssertEqual(positions.count, 3)
25+
26+
let expectedEdits = [
27+
SourceEdit(range: positions["1️⃣"]!..<positions["2️⃣"]!, replacement: "Multiident "),
28+
SourceEdit(range: positions["2️⃣"]!..<positions["3️⃣"]!, replacement: ""),
29+
]
30+
let tree = Parser.parse(source: source)
31+
let diags = ParseDiagnosticsGenerator.diagnostics(for: tree)
32+
XCTAssertEqual(diags.count, 1)
33+
let diag = try XCTUnwrap(diags.first)
34+
XCTAssertEqual(diag.fixIts.count, 2)
35+
36+
let fixIt = try XCTUnwrap(diag.fixIts.first)
37+
let changes = fixIt.changes
38+
let edits = fixIt.edits
39+
XCTAssertNotEqual(changes.count, edits.count)
40+
XCTAssertEqual(expectedEdits, edits)
41+
}
42+
}

0 commit comments

Comments
 (0)