@@ -13,6 +13,7 @@ import (
13
13
"go/parser"
14
14
"go/token"
15
15
"go/types"
16
+ "sort"
16
17
"strings"
17
18
18
19
"golang.org/x/tools/go/analysis"
@@ -38,6 +39,13 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
38
39
return nil , nil , fmt .Errorf ("nil interface request" )
39
40
}
40
41
42
+ // A function-local type cannot be stubbed
43
+ // since there's nowhere to put the methods.
44
+ conc := si .Concrete .Obj ()
45
+ if conc != conc .Pkg ().Scope ().Lookup (conc .Name ()) {
46
+ return nil , nil , fmt .Errorf ("local type %q cannot be stubbed" , conc .Name ())
47
+ }
48
+
41
49
// Parse the file defining the concrete type.
42
50
concreteFilename := snapshot .FileSet ().Position (si .Concrete .Obj ().Pos ()).Filename
43
51
concreteFH , err := snapshot .GetFile (ctx , span .URIFromPath (concreteFilename ))
@@ -56,24 +64,36 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
56
64
methodsSrc = stubErr (ctx , parsedConcreteFile .File , si , snapshot )
57
65
} else {
58
66
methodsSrc , stubImports , err = stubMethods (ctx , parsedConcreteFile .File , si , snapshot )
67
+ if err != nil {
68
+ return nil , nil , fmt .Errorf ("stubMethods: %w" , err )
69
+ }
59
70
}
60
- if err != nil {
61
- return nil , nil , fmt .Errorf ("stubMethods: %w" , err )
71
+
72
+ // Splice the methods into the file.
73
+ // The insertion point is after the top-level declaration
74
+ // enclosing the (package-level) type object.
75
+ insertPos := parsedConcreteFile .File .End ()
76
+ for _ , decl := range parsedConcreteFile .File .Decls {
77
+ if decl .End () > conc .Pos () {
78
+ insertPos = decl .End ()
79
+ break
80
+ }
62
81
}
63
- nodes , _ = astutil .PathEnclosingInterval (parsedConcreteFile .File , si .Concrete .Obj ().Pos (), si .Concrete .Obj ().Pos ())
64
82
concreteSrc , err := concreteFH .Read ()
65
83
if err != nil {
66
84
return nil , nil , fmt .Errorf ("error reading concrete file source: %w" , err )
67
85
}
68
- insertPos , err := safetoken .Offset (parsedConcreteFile .Tok , nodes [ 1 ]. End () )
69
- if err != nil || insertPos >= len (concreteSrc ) {
86
+ insertOffset , err := safetoken .Offset (parsedConcreteFile .Tok , insertPos )
87
+ if err != nil || insertOffset >= len (concreteSrc ) {
70
88
return nil , nil , fmt .Errorf ("insertion position is past the end of the file" )
71
89
}
72
90
var buf bytes.Buffer
73
- buf .Write (concreteSrc [:insertPos ])
91
+ buf .Write (concreteSrc [:insertOffset ])
74
92
buf .WriteByte ('\n' )
75
93
buf .Write (methodsSrc )
76
- buf .Write (concreteSrc [insertPos :])
94
+ buf .Write (concreteSrc [insertOffset :])
95
+
96
+ // Re-parse it, splice in imports, pretty-print it.
77
97
fset := token .NewFileSet ()
78
98
newF , err := parser .ParseFile (fset , parsedConcreteFile .File .Name .Name , buf .Bytes (), parser .ParseComments )
79
99
if err != nil {
@@ -82,11 +102,12 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
82
102
for _ , imp := range stubImports {
83
103
astutil .AddNamedImport (fset , newF , imp .Name , imp .Path )
84
104
}
85
- var source bytes.Buffer
86
- err = format .Node (& source , fset , newF )
87
- if err != nil {
105
+ var source strings.Builder
106
+ if err := format .Node (& source , fset , newF ); err != nil {
88
107
return nil , nil , fmt .Errorf ("format.Node: %w" , err )
89
108
}
109
+
110
+ // Return the diff.
90
111
diffs := snapshot .View ().Options ().ComputeEdits (string (parsedConcreteFile .Src ), source .String ())
91
112
tf := parsedConcreteFile .Mapper .TokFile
92
113
var edits []analysis.TextEdit
@@ -234,21 +255,11 @@ returns
234
255
},
235
256
}
236
257
*/
237
- func missingMethods (ctx context.Context , snapshot Snapshot , concMS * types.MethodSet , concPkg * types.Package , ifaceObj types.Object , visited map [string ]struct {}) ([]* missingInterface , error ) {
258
+ func missingMethods (ctx context.Context , snapshot Snapshot , concMS * types.MethodSet , concPkg * types.Package , ifaceObj * types.TypeName , visited map [string ]struct {}) ([]* missingInterface , error ) {
238
259
iface , ok := ifaceObj .Type ().Underlying ().(* types.Interface )
239
260
if ! ok {
240
261
return nil , fmt .Errorf ("expected %v to be an interface but got %T" , iface , ifaceObj .Type ().Underlying ())
241
262
}
242
- missing := []* missingInterface {}
243
- for i := 0 ; i < iface .NumEmbeddeds (); i ++ {
244
- eiface := iface .Embedded (i ).Obj ()
245
- em , err := missingMethods (ctx , snapshot , concMS , concPkg , eiface , visited )
246
- if err != nil {
247
- return nil , err
248
- }
249
- missing = append (missing , em ... )
250
- }
251
-
252
263
// Parse the imports from the file that declares the interface.
253
264
ifaceFilename := snapshot .FileSet ().Position (ifaceObj .Pos ()).Filename
254
265
ifaceFH , err := snapshot .GetFile (ctx , span .URIFromPath (ifaceFilename ))
@@ -259,27 +270,55 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method
259
270
if err != nil {
260
271
return nil , fmt .Errorf ("error parsing imports from interface file: %w" , err )
261
272
}
273
+
262
274
mi := & missingInterface {
263
- iface : iface ,
275
+ iface : ifaceObj ,
264
276
imports : ifaceFile .File .Imports ,
265
277
}
278
+
279
+ // Add all the interface methods not defined by the concrete type to mi.missing.
266
280
for i := 0 ; i < iface .NumExplicitMethods (); i ++ {
267
281
method := iface .ExplicitMethod (i )
268
- // if the concrete type does not have the interface method
269
- if concMS . Lookup ( concPkg , method . Name ()) == nil {
282
+ if sel := concMS . Lookup ( concPkg , method . Name ()); sel == nil {
283
+ // Concrete type does not have the interface method.
270
284
if _ , ok := visited [method .Name ()]; ! ok {
271
285
mi .missing = append (mi .missing , method )
272
286
visited [method .Name ()] = struct {}{}
273
287
}
274
- }
275
- if sel := concMS . Lookup ( concPkg , method . Name ()); sel != nil {
288
+ } else {
289
+ // Concrete type does have the interface method.
276
290
implSig := sel .Type ().(* types.Signature )
277
291
ifaceSig := method .Type ().(* types.Signature )
278
292
if ! types .Identical (ifaceSig , implSig ) {
279
293
return nil , fmt .Errorf ("mimsatched %q function signatures:\n have: %s\n want: %s" , method .Name (), implSig , ifaceSig )
280
294
}
281
295
}
282
296
}
297
+
298
+ // Process embedded interfaces, recursively.
299
+ //
300
+ // TODO(adonovan): this whole computation could be expressed
301
+ // more simply without recursion, driven by the method
302
+ // sets of the interface and concrete types. Once the set
303
+ // difference (missing methods) is computed, the imports
304
+ // from the declaring file(s) could be loaded as needed.
305
+ var missing []* missingInterface
306
+ for i := 0 ; i < iface .NumEmbeddeds (); i ++ {
307
+ eiface := iface .Embedded (i ).Obj ()
308
+ em , err := missingMethods (ctx , snapshot , concMS , concPkg , eiface , visited )
309
+ if err != nil {
310
+ return nil , err
311
+ }
312
+ missing = append (missing , em ... )
313
+ }
314
+ // The type checker is deterministic, but its choice of
315
+ // ordering of embedded interfaces varies with Go version
316
+ // (e.g. go1.17 was sorted, go1.18 was lexical order).
317
+ // Sort to ensure test portability.
318
+ sort .Slice (missing , func (i , j int ) bool {
319
+ return missing [i ].iface .Id () < missing [j ].iface .Id ()
320
+ })
321
+
283
322
if len (mi .missing ) > 0 {
284
323
missing = append (missing , mi )
285
324
}
@@ -290,7 +329,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method
290
329
// that has all or some of its methods missing
291
330
// from the destination concrete type
292
331
type missingInterface struct {
293
- iface * types.Interface
332
+ iface * types.TypeName
294
333
imports []* ast.ImportSpec // the interface's import environment
295
334
missing []* types.Func
296
335
}
0 commit comments