12
12
13
13
import SwiftSyntax
14
14
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
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: Input , in context: Context ) -> [ SourceEdit ]
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: Input ) -> [ SourceEdit ] {
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.
39
52
///
40
53
/// - Note: The syntax trees returned by SwiftSyntax are immutable: any
41
54
/// transformation made against the tree results in a distinct tree.
@@ -44,43 +57,116 @@ import SwiftSyntax
44
57
/// =========================
45
58
///
46
59
/// 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
48
61
/// from a variety of erroneous inputs, a refactoring provider has to be
49
62
/// prepared to fail gracefully as well. Many refactoring providers follow a
50
63
/// common validation pattern that "preflights" the refactoring by testing the
51
64
/// 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 {
56
- /// The type of syntax this refactoring action accepts.
57
- associatedtype Input : SyntaxProtocol = SourceFileSyntax
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 {
69
+ // Should not be required, see https://github.com/apple/swift/issues/66004.
70
+ // The default is a hack to workaround the warning that we'd hit otherwise.
71
+ associatedtype Input : SyntaxProtocol = MissingSyntax
58
72
/// The type of syntax this refactoring action returns.
59
- associatedtype Output : SyntaxProtocol = SourceFileSyntax
73
+ associatedtype Output : SyntaxProtocol
60
74
/// Contextual information used by the refactoring action.
61
75
associatedtype Context = Void
62
76
63
- /// Perform the refactoring action on the provided syntax node.
77
+ /// Perform the refactoring action on the provided syntax node. It is assumed
78
+ /// that the returned output completely replaces the input node.
64
79
///
65
80
/// - Parameters:
66
81
/// - syntax: The syntax to transform.
67
82
/// - context: Contextual information used by the refactoring action.
68
83
/// - Returns: The result of applying the refactoring action, or `nil` if the
69
84
/// action could not be performed.
70
- static func refactor( syntax: Self . Input , in context: Self . Context ) -> Self . Output ?
85
+ static func refactor( syntax: Input , in context: Context ) -> Output ?
71
86
}
72
87
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.
88
+ extension SyntaxRefactoringProvider where Context == Void {
89
+ /// See `refactor(syntax:in:)`. This method provides a convenient way to
90
+ /// invoke a refactoring action that requires no context.
78
91
///
79
92
/// - Parameters:
80
93
/// - syntax: The syntax to transform.
81
94
/// - Returns: The result of applying the refactoring action, or `nil` if the
82
95
/// action could not be performed.
83
- public static func refactor( syntax: Self . Input ) -> Self . Output ? {
96
+ public static func refactor( syntax: Input ) -> Output ? {
84
97
return self . refactor ( syntax: syntax, in: ( ) )
85
98
}
86
99
}
100
+
101
+ extension SyntaxRefactoringProvider {
102
+ /// Provides a default implementation for
103
+ /// `EditRefactoringProvider.textRefactor(syntax:in:)` that produces an edit
104
+ /// to replace the input of `refactor(syntax:in:)` with its returned output.
105
+ public static func textRefactor( syntax: Input , in context: Context ) -> [ SourceEdit ] {
106
+ guard let output = refactor ( syntax: syntax, in: context) else {
107
+ return [ ]
108
+ }
109
+ return [ SourceEdit . replace ( syntax, with: output. description) ]
110
+ }
111
+ }
112
+
113
+ /// An 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
+ }
0 commit comments