Skip to content

Commit 627d033

Browse files
committed
Add a EditRefactoringProvider which provides textual edits
1 parent 7fbb726 commit 627d033

11 files changed

+261
-89
lines changed

Sources/SwiftRefactor/AddSeparatorsToIntegerLiteral.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import SwiftSyntax
3232
/// 0xF_FFFF_FFFF
3333
/// 0b1_010
3434
/// ```
35-
public struct AddSeparatorsToIntegerLiteral: RefactoringProvider {
35+
public struct AddSeparatorsToIntegerLiteral: SyntaxRefactoringProvider {
3636
public static func refactor(syntax lit: IntegerLiteralExprSyntax, in context: Void) -> IntegerLiteralExprSyntax? {
3737
if lit.digits.text.contains("_") {
3838
guard let strippedLiteral = RemoveSeparatorsFromIntegerLiteral.refactor(syntax: lit) else {

Sources/SwiftRefactor/FormatRawStringLiteral.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import SwiftSyntax
3030
/// ##"Hello \#(world)"##
3131
/// "Hello World"
3232
/// ```
33-
public struct FormatRawStringLiteral: RefactoringProvider {
33+
public struct FormatRawStringLiteral: SyntaxRefactoringProvider {
3434
public static func refactor(syntax lit: StringLiteralExprSyntax, in context: Void) -> StringLiteralExprSyntax? {
3535
var maximumHashes = 0
3636
for segment in lit.segments {

Sources/SwiftRefactor/MigrateToNewIfLetSyntax.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import SwiftParser
3333
/// if let foo {
3434
/// // ...
3535
/// }
36-
public struct MigrateToNewIfLetSyntax: RefactoringProvider {
36+
public struct MigrateToNewIfLetSyntax: SyntaxRefactoringProvider {
3737
public static func refactor(syntax node: IfExprSyntax, in context: ()) -> IfExprSyntax? {
3838
// Visit all conditions in the node.
3939
let newConditions = node.conditions.enumerated().map { (index, condition) -> ConditionElementListSyntax.Element in

Sources/SwiftRefactor/OpaqueParameterToGeneric.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ fileprivate class SomeParameterRewriter: SyntaxRewriter {
115115
/// ```swift
116116
/// func someFunction<T1: Value>(_ input: T1) {}
117117
/// ```
118-
public struct OpaqueParameterToGeneric: RefactoringProvider {
118+
public struct OpaqueParameterToGeneric: SyntaxRefactoringProvider {
119119
/// Replace all of the "some" parameters in the given parameter clause with
120120
/// freshly-created generic parameters.
121121
///

Sources/SwiftRefactor/RefactoringProvider.swift

Lines changed: 124 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,43 @@
1212

1313
import SwiftSyntax
1414

15-
/// A type that transforms syntax to provide a (context-sensitive)
16-
/// refactoring.
17-
///
18-
/// A type conforming to the `RefactoringProvider` protocol defines
19-
/// a refactoring action against a family of Swift syntax trees.
20-
///
21-
/// Refactoring
22-
/// ===========
23-
///
24-
/// Refactoring is the act of transforming source code to be more effective.
25-
/// A refactoring does not affect the semantics of code it is transforming.
26-
/// Rather, it makes that code easier to read and reason about.
27-
///
28-
/// Code Transformation
29-
/// ===================
30-
///
31-
/// Refactoring is expressed as structural transformations of Swift
32-
/// syntax trees. The SwiftSyntax API provides a natural, easy-to-use,
33-
/// and compositional set of updates to the syntax tree. For example, a
34-
/// refactoring action that wishes to exchange the leading trivia of a node
35-
/// would call `with(\.leadingTrivia, _:)` against its input syntax and return
36-
/// the resulting syntax node. For compound syntax nodes, entire sub-trees
37-
/// can be added, exchanged, or removed by calling the corresponding `with`
38-
/// API.
15+
/// A refactoring expressed as textual edits on the original syntax tree. In
16+
/// general clients should prefer `SyntaxRefactoringProvider` where possible.
17+
public protocol EditRefactoringProvider {
18+
/// The type of syntax this refactoring action accepts.
19+
associatedtype Input: SyntaxProtocol = SourceFileSyntax
20+
/// Contextual information used by the refactoring action.
21+
associatedtype Context = Void
22+
23+
/// Perform the refactoring action on the provided syntax node.
24+
///
25+
/// - Parameters:
26+
/// - syntax: The syntax to transform.
27+
/// - context: Contextual information used by the refactoring action.
28+
/// - Returns: Textual edits that describe how to apply the result of the
29+
/// refactoring action on locations within the original tree. An
30+
/// empty array if the refactoring could not be performed.
31+
static func textRefactor(syntax: Self.Input, in context: Self.Context) -> [Edit]
32+
}
33+
34+
extension EditRefactoringProvider where Context == Void {
35+
/// See `textRefactor(syntax:in:)`. This method provides a convenient way to
36+
/// invoke a refactoring action that requires no context.
37+
///
38+
/// - Parameters:
39+
/// - syntax: The syntax to transform.
40+
/// - Returns: Textual edits describing the refactoring to perform.
41+
public static func textRefactor(syntax: Self.Input) -> [Edit] {
42+
return self.textRefactor(syntax: syntax, in: ())
43+
}
44+
}
45+
46+
/// A refactoring expressed as a structural transformation of the original
47+
/// syntax node. For example, a refactoring action that wishes to exchange the
48+
/// leading trivia of a node could call call `with(\.leadingTrivia, _:)`
49+
/// against its input syntax and return the resulting syntax node. Or, for
50+
/// compound syntax nodes, entire sub-trees can be added, exchanged, or removed
51+
/// by calling the corresponding `with` API.
3952
///
4053
/// - Note: The syntax trees returned by SwiftSyntax are immutable: any
4154
/// transformation made against the tree results in a distinct tree.
@@ -44,23 +57,24 @@ import SwiftSyntax
4457
/// =========================
4558
///
4659
/// A refactoring provider cannot assume that the syntax it is given is
47-
/// neessarily well-formed. As the SwiftSyntax library is capable of recovering
60+
/// necessarily well-formed. As the SwiftSyntax library is capable of recovering
4861
/// from a variety of erroneous inputs, a refactoring provider has to be
4962
/// prepared to fail gracefully as well. Many refactoring providers follow a
5063
/// common validation pattern that "preflights" the refactoring by testing the
5164
/// structure of the provided syntax nodes. If the tests fail, the refactoring
52-
/// provider exits early by returning `nil`. It is recommended that refactoring
53-
/// actions fail as quickly as possible to give any associated tooling
54-
/// space to recover as well.
55-
public protocol RefactoringProvider {
65+
/// provider exits early by returning an empty array. It is recommended that
66+
/// refactoring actions fail as quickly as possible to give any associated
67+
/// tooling space to recover as well.
68+
public protocol SyntaxRefactoringProvider: EditRefactoringProvider {
5669
/// The type of syntax this refactoring action accepts.
5770
associatedtype Input: SyntaxProtocol = SourceFileSyntax
5871
/// The type of syntax this refactoring action returns.
5972
associatedtype Output: SyntaxProtocol = SourceFileSyntax
6073
/// Contextual information used by the refactoring action.
6174
associatedtype Context = Void
6275

63-
/// Perform the refactoring action on the provided syntax node.
76+
/// Perform the refactoring action on the provided syntax node. It is assumed
77+
/// that the returned output completely replaces the input node.
6478
///
6579
/// - Parameters:
6680
/// - syntax: The syntax to transform.
@@ -70,11 +84,9 @@ public protocol RefactoringProvider {
7084
static func refactor(syntax: Self.Input, in context: Self.Context) -> Self.Output?
7185
}
7286

73-
extension RefactoringProvider where Context == Void {
74-
/// Perform the refactoring action on the provided syntax node.
75-
///
76-
/// This method provides a convenient way to invoke a refactoring action that
77-
/// requires no context.
87+
extension SyntaxRefactoringProvider where Context == Void {
88+
/// See `refactor(syntax:in:)`. This method provides a convenient way to
89+
/// invoke a refactoring action that requires no context.
7890
///
7991
/// - Parameters:
8092
/// - syntax: The syntax to transform.
@@ -84,3 +96,80 @@ extension RefactoringProvider where Context == Void {
8496
return self.refactor(syntax: syntax, in: ())
8597
}
8698
}
99+
100+
extension SyntaxRefactoringProvider {
101+
/// Provides a default implementation for
102+
/// `EditRefactoringProvider.textRefactor(syntax:in:)` that produces an edit
103+
/// to replace the input of `refactor(syntax:in:)` with its returned output.
104+
public static func textRefactor(syntax: Self.Input, in context: Self.Context) -> [Edit] {
105+
guard let output = refactor(syntax: syntax, in: context) else {
106+
return []
107+
}
108+
return [Edit.replace(syntax, with: output.description)]
109+
}
110+
}
111+
112+
/// An textual edit to the original source represented by a range and a
113+
/// replacement.
114+
public struct Edit: Equatable {
115+
/// The location in the original source that this edit starts at.
116+
public let start: AbsolutePosition
117+
/// The (closed) end of this edit in the original source. Equal to the start
118+
/// for an addition.
119+
public let end: AbsolutePosition
120+
/// The text to replace the original range with. Empty for a deletion.
121+
public let replacement: String
122+
123+
/// Length of the original source range that this edit applies to. Zero if
124+
/// this is an addition.
125+
public var length: SourceLength {
126+
return SourceLength(utf8Length: end.utf8Offset - start.utf8Offset)
127+
}
128+
129+
/// Create an edit to replace `start` to (closed) `end` in the original
130+
/// source with `replacement`.
131+
public init(start: AbsolutePosition, end: AbsolutePosition, replacement: String) {
132+
self.start = start
133+
self.end = end
134+
self.replacement = replacement
135+
}
136+
137+
/// Convenience function to create a textual addition after the given node
138+
/// and its trivia.
139+
public static func insert<T: SyntaxProtocol>(_ newText: String, after node: T) -> Edit {
140+
return Edit(start: node.endPosition, end: node.endPosition, replacement: newText)
141+
}
142+
143+
/// Convenience function to create a textual addition before the given node
144+
/// and its trivia.
145+
public static func insert<T: SyntaxProtocol>(_ newText: String, before node: T) -> Edit {
146+
return Edit(start: node.position, end: node.position, replacement: newText)
147+
}
148+
149+
/// Convenience function to create a textual replacement of the given node,
150+
/// including its trivia.
151+
public static func replace<T: SyntaxProtocol>(_ node: T, with replacement: String) -> Edit {
152+
return Edit(start: node.position, end: node.endPosition, replacement: replacement)
153+
}
154+
155+
/// Convenience function to create a textual deletion the given node and its
156+
/// trivia.
157+
public static func remove<T: SyntaxProtocol>(_ node: T) -> Edit {
158+
return Edit(start: node.position, end: node.endPosition, replacement: "")
159+
}
160+
}
161+
162+
extension Edit: CustomStringConvertible {
163+
public var description: String {
164+
let hasNewline = replacement.contains { $0.isNewline }
165+
if hasNewline {
166+
return #"""
167+
\#(start.utf8Offset)-\#(end.utf8Offset)
168+
"""
169+
\#(replacement)
170+
"""
171+
"""#
172+
}
173+
return "\(start.utf8Offset)-\(end.utf8Offset) \"\(replacement)\""
174+
}
175+
}

Sources/SwiftRefactor/RemoveSeparatorsFromIntegerLiteral.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import SwiftSyntax
2626
/// 123456789
2727
/// 0xFFFFFFFFF
2828
/// ```
29-
public struct RemoveSeparatorsFromIntegerLiteral: RefactoringProvider {
29+
public struct RemoveSeparatorsFromIntegerLiteral: SyntaxRefactoringProvider {
3030
public static func refactor(syntax lit: IntegerLiteralExprSyntax, in context: Void) -> IntegerLiteralExprSyntax? {
3131
guard lit.digits.text.contains("_") else {
3232
return lit

Tests/SwiftRefactorTest/FormatRawStringLiteral.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ final class FormatRawStringLiteralTest: XCTestCase {
3434
for (line, literal, expectation) in tests {
3535
let literal = try XCTUnwrap(StringLiteralExprSyntax.parseWithoutDiagnostics(from: literal))
3636
let expectation = try XCTUnwrap(StringLiteralExprSyntax.parseWithoutDiagnostics(from: expectation))
37-
let refactored = try XCTUnwrap(FormatRawStringLiteral.refactor(syntax: literal))
38-
assertStringsEqualWithDiff(refactored.description, expectation.description, line: UInt(line))
37+
try assertRefactor(literal, context: (), provider: FormatRawStringLiteral.self, expected: expectation, line: UInt(line))
3938
}
4039
}
4140
}

Tests/SwiftRefactorTest/MigrateToNewIfLetSyntax.swift

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,6 @@ import SwiftSyntaxBuilder
1717
import XCTest
1818
import _SwiftSyntaxTestSupport
1919

20-
func assertRefactorIfLet(
21-
_ syntax: ExprSyntax,
22-
expected: ExprSyntax,
23-
file: StaticString = #file,
24-
line: UInt = #line
25-
) throws {
26-
let ifExpr = try XCTUnwrap(
27-
syntax.as(IfExprSyntax.self),
28-
file: file,
29-
line: line
30-
)
31-
32-
let refactored = try XCTUnwrap(
33-
MigrateToNewIfLetSyntax.refactor(syntax: ifExpr),
34-
file: file,
35-
line: line
36-
)
37-
38-
assertStringsEqualWithDiff(
39-
expected.description,
40-
refactored.description,
41-
file: file,
42-
line: line
43-
)
44-
}
45-
4620
final class MigrateToNewIfLetSyntaxTest: XCTestCase {
4721
func testRefactoring() throws {
4822
let baselineSyntax: ExprSyntax = """
@@ -53,7 +27,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
5327
if let x {}
5428
"""
5529

56-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
30+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
5731
}
5832

5933
func testIdempotence() throws {
@@ -65,8 +39,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
6539
if let x {}
6640
"""
6741

68-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
69-
try assertRefactorIfLet(expectedSyntax, expected: expectedSyntax)
42+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
7043
}
7144

7245
func testMultiBinding() throws {
@@ -78,7 +51,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
7851
if let x, var y, let z {}
7952
"""
8053

81-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
54+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
8255
}
8356

8457
func testMixedBinding() throws {
@@ -90,7 +63,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
9063
if let x, var y = x, let z = y.w {}
9164
"""
9265

93-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
66+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
9467
}
9568

9669
func testConditions() throws {
@@ -102,7 +75,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
10275
if let x = x + 1, x == x, !x {}
10376
"""
10477

105-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
78+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
10679
}
10780

10881
func testWhitespaceNormalization() throws {
@@ -114,7 +87,7 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
11487
if let x, let y {}
11588
"""
11689

117-
try assertRefactorIfLet(baselineSyntax, expected: expectedSyntax)
90+
try assertRefactor(baselineSyntax, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
11891
}
11992

12093
func testIfStmt() throws {
@@ -127,6 +100,6 @@ final class MigrateToNewIfLetSyntaxTest: XCTestCase {
127100
"""
128101

129102
let exprStmt = try XCTUnwrap(baselineSyntax.as(ExpressionStmtSyntax.self))
130-
try assertRefactorIfLet(exprStmt.expression, expected: expectedSyntax)
103+
try assertRefactor(exprStmt.expression, context: (), provider: MigrateToNewIfLetSyntax.self, expected: expectedSyntax)
131104
}
132105
}

Tests/SwiftRefactorTest/OpaqueParameterToGeneric.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ final class OpaqueParameterToGenericTest: XCTestCase {
3333
) -> some Equatable { }
3434
"""
3535

36-
let refactored = try XCTUnwrap(OpaqueParameterToGeneric.refactor(syntax: baseline))
37-
38-
assertStringsEqualWithDiff(expected.description, refactored.description)
36+
try assertRefactor(baseline, context: (), provider: OpaqueParameterToGeneric.self, expected: expected)
3937
}
4038

4139
func testRefactoringInit() throws {
@@ -53,9 +51,7 @@ final class OpaqueParameterToGenericTest: XCTestCase {
5351
) { }
5452
"""
5553

56-
let refactored = try XCTUnwrap(OpaqueParameterToGeneric.refactor(syntax: baseline))
57-
58-
assertStringsEqualWithDiff(expected.description, refactored.description)
54+
try assertRefactor(baseline, context: (), provider: OpaqueParameterToGeneric.self, expected: expected)
5955
}
6056

6157
func testRefactoringSubscript() throws {
@@ -67,8 +63,6 @@ final class OpaqueParameterToGenericTest: XCTestCase {
6763
subscript<T1: Hashable>(index: T1) -> String
6864
"""
6965

70-
let refactored = try XCTUnwrap(OpaqueParameterToGeneric.refactor(syntax: baseline))
71-
72-
assertStringsEqualWithDiff(expected.description, refactored.description)
66+
try assertRefactor(baseline, context: (), provider: OpaqueParameterToGeneric.self, expected: expected)
7367
}
7468
}

0 commit comments

Comments
 (0)