5
5
package golang
6
6
7
7
import (
8
+ "bytes"
8
9
"context"
10
+ "errors"
9
11
"fmt"
10
12
"go/ast"
11
13
"go/token"
12
14
"go/types"
15
+ "slices"
13
16
14
17
"golang.org/x/tools/go/analysis"
18
+ "golang.org/x/tools/go/ast/astutil"
15
19
"golang.org/x/tools/gopls/internal/analysis/embeddirective"
16
20
"golang.org/x/tools/gopls/internal/analysis/fillstruct"
17
21
"golang.org/x/tools/gopls/internal/analysis/stubmethods"
@@ -22,6 +26,7 @@ import (
22
26
"golang.org/x/tools/gopls/internal/file"
23
27
"golang.org/x/tools/gopls/internal/protocol"
24
28
"golang.org/x/tools/gopls/internal/util/bug"
29
+ "golang.org/x/tools/gopls/internal/util/safetoken"
25
30
"golang.org/x/tools/internal/imports"
26
31
)
27
32
@@ -61,6 +66,7 @@ func singleFile(fixer1 singleFileFixer) fixer {
61
66
const (
62
67
fixExtractVariable = "extract_variable"
63
68
fixExtractFunction = "extract_function"
69
+ fixExtractInterface = "extract_interface"
64
70
fixExtractMethod = "extract_method"
65
71
fixInlineCall = "inline_call"
66
72
fixInvertIfCondition = "invert_if_condition"
@@ -112,6 +118,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
112
118
113
119
// Ad-hoc fixers: these are used when the command is
114
120
// constructed directly by logic in server/code_action.
121
+ fixExtractInterface : extractInterface ,
115
122
fixExtractFunction : singleFile (extractFunction ),
116
123
fixExtractMethod : singleFile (extractMethod ),
117
124
fixExtractVariable : singleFile (extractVariable ),
@@ -142,6 +149,140 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
142
149
return suggestedFixToEdits (ctx , snapshot , fixFset , suggestion )
143
150
}
144
151
152
+ func extractInterface (ctx context.Context , snapshot * cache.Snapshot , pkg * cache.Package , pgf * parsego.File , start , end token.Pos ) (* token.FileSet , * analysis.SuggestedFix , error ) {
153
+ path , _ := astutil .PathEnclosingInterval (pgf .File , start , end )
154
+
155
+ var field * ast.Field
156
+ var decl ast.Decl
157
+ for _ , node := range path {
158
+ if f , ok := node .(* ast.Field ); ok {
159
+ field = f
160
+ continue
161
+ }
162
+
163
+ // Record the node that starts the declaration of the type that contains
164
+ // the field we are creating the interface for.
165
+ if d , ok := node .(ast.Decl ); ok {
166
+ decl = d
167
+ break // we have both the field and the declaration
168
+ }
169
+ }
170
+
171
+ if field == nil || decl == nil {
172
+ return nil , nil , nil
173
+ }
174
+
175
+ p := safetoken .StartPosition (pkg .FileSet (), field .Pos ())
176
+ pos := protocol.Position {
177
+ Line : uint32 (p .Line - 1 ), // Line is zero-based
178
+ Character : uint32 (p .Column - 1 ), // Character is zero-based
179
+ }
180
+
181
+ fh , err := snapshot .ReadFile (ctx , pgf .URI )
182
+ if err != nil {
183
+ return nil , nil , err
184
+ }
185
+
186
+ refs , err := references (ctx , snapshot , fh , pos , false )
187
+ if err != nil {
188
+ return nil , nil , err
189
+ }
190
+
191
+ type method struct {
192
+ signature * types.Signature
193
+ name string
194
+ }
195
+
196
+ var methods []method
197
+ for _ , ref := range refs {
198
+ locPkg , locPgf , err := NarrowestPackageForFile (ctx , snapshot , ref .location .URI )
199
+ if err != nil {
200
+ return nil , nil , err
201
+ }
202
+
203
+ _ , end , err := locPgf .RangePos (ref .location .Range )
204
+ if err != nil {
205
+ return nil , nil , err
206
+ }
207
+
208
+ // We are interested in the method call, so we need the node after the dot
209
+ rangeEnd := end + token .Pos (len ("." ))
210
+ path , _ := astutil .PathEnclosingInterval (locPgf .File , rangeEnd , rangeEnd )
211
+ id , ok := path [0 ].(* ast.Ident )
212
+ if ! ok {
213
+ continue
214
+ }
215
+
216
+ obj := locPkg .TypesInfo ().ObjectOf (id )
217
+ if obj == nil {
218
+ continue
219
+ }
220
+
221
+ sig , ok := obj .Type ().(* types.Signature )
222
+ if ! ok {
223
+ return nil , nil , errors .New ("cannot extract interface with non-method accesses" )
224
+ }
225
+
226
+ fc := method {signature : sig , name : obj .Name ()}
227
+ if ! slices .Contains (methods , fc ) {
228
+ methods = append (methods , fc )
229
+ }
230
+ }
231
+
232
+ interfaceName := "I" + pkg .TypesInfo ().ObjectOf (field .Names [0 ]).Name ()
233
+ var buf bytes.Buffer
234
+ buf .WriteString ("\n type " )
235
+ buf .WriteString (interfaceName )
236
+ buf .WriteString (" interface {\n " )
237
+ for _ , fc := range methods {
238
+ buf .WriteString ("\t " )
239
+ buf .WriteString (fc .name )
240
+ types .WriteSignature (& buf , fc .signature , relativeTo (pkg .Types ()))
241
+ buf .WriteByte ('\n' )
242
+ }
243
+ buf .WriteByte ('}' )
244
+ buf .WriteByte ('\n' )
245
+
246
+ interfacePos := decl .Pos () - 1
247
+ // Move the interface above the documentation comment if the type declaration
248
+ // includes one.
249
+ switch d := decl .(type ) {
250
+ case * ast.GenDecl :
251
+ if d .Doc != nil {
252
+ interfacePos = d .Doc .Pos () - 1
253
+ }
254
+ case * ast.FuncDecl :
255
+ if d .Doc != nil {
256
+ interfacePos = d .Doc .Pos () - 1
257
+ }
258
+ }
259
+
260
+ return pkg .FileSet (), & analysis.SuggestedFix {
261
+ Message : "Extract interface" ,
262
+ TextEdits : []analysis.TextEdit {{
263
+ Pos : interfacePos ,
264
+ End : interfacePos ,
265
+ NewText : buf .Bytes (),
266
+ }, {
267
+ Pos : field .Type .Pos (),
268
+ End : field .Type .End (),
269
+ NewText : []byte (interfaceName ),
270
+ }},
271
+ }, nil
272
+ }
273
+
274
+ func relativeTo (pkg * types.Package ) types.Qualifier {
275
+ if pkg == nil {
276
+ return nil
277
+ }
278
+ return func (other * types.Package ) string {
279
+ if pkg == other {
280
+ return "" // same package; unqualified
281
+ }
282
+ return other .Name ()
283
+ }
284
+ }
285
+
145
286
// suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form.
146
287
func suggestedFixToEdits (ctx context.Context , snapshot * cache.Snapshot , fset * token.FileSet , suggestion * analysis.SuggestedFix ) ([]protocol.TextDocumentEdit , error ) {
147
288
editsPerFile := map [protocol.DocumentURI ]* protocol.TextDocumentEdit {}
0 commit comments