1212
1313import 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
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.
3952///
4053/// - Note: The syntax trees returned by SwiftSyntax are immutable: any
4154/// transformation made against the tree results in a distinct tree.
@@ -44,43 +57,116 @@ 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 {
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
5872 /// The type of syntax this refactoring action returns.
59- associatedtype Output : SyntaxProtocol = SourceFileSyntax
73+ associatedtype Output : SyntaxProtocol
6074 /// Contextual information used by the refactoring action.
6175 associatedtype Context = Void
6276
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.
6479 ///
6580 /// - Parameters:
6681 /// - syntax: The syntax to transform.
6782 /// - context: Contextual information used by the refactoring action.
6883 /// - Returns: The result of applying the refactoring action, or `nil` if the
6984 /// 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 ?
7186}
7287
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.
7891 ///
7992 /// - Parameters:
8093 /// - syntax: The syntax to transform.
8194 /// - Returns: The result of applying the refactoring action, or `nil` if the
8295 /// action could not be performed.
83- public static func refactor( syntax: Self . Input ) -> Self . Output ? {
96+ public static func refactor( syntax: Input ) -> Output ? {
8497 return self . refactor ( syntax: syntax, in: ( ) )
8598 }
8699}
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