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