Skip to content

Commit 9ec8553

Browse files
committed
gopls/internal/lsp/source: emit interface stub methods at top level
Before, the insertion point of the new methods was after the TypeSpec that declared the concrete type, but that isn't valid if the containing GenDecl is parenthesized. This change causes it to insert the methods after the decl. Also - a regression test - a would-be test of attempting to stub a function-local type, but the test framework cannot express errors yet. - sort embedded interfaces, since go/types changed its sort order in go1.18, and this CL's new test is the first time it has been exercised with an interface (io.ReadCloser) that has more than one embedded interface. Needless to say, figuring that out took 10x more time than actually fixing the reported bug. Fixes golang/go#56825 Change-Id: I0fc61f297befd12e8a7224dcbd6a197cf9e4b1cf Reviewed-on: https://go-review.googlesource.com/c/tools/+/456316 Run-TryBot: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Suzy Mueller <[email protected]>
1 parent 444c8f6 commit 9ec8553

File tree

6 files changed

+137
-32
lines changed

6 files changed

+137
-32
lines changed

gopls/internal/lsp/analysis/stubmethods/stubmethods.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ type StubInfo struct {
8484
// in the case where the concrete type file requires a new import that happens to be renamed
8585
// in the interface file.
8686
// TODO(marwan-at-work): implement interface literals.
87-
Interface types.Object
87+
Interface *types.TypeName
8888
Concrete *types.Named
8989
Pointer bool
9090
}
@@ -335,15 +335,15 @@ func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports []
335335
// ifaceType will try to extract the types.Object that defines
336336
// the interface given the ast.Expr where the "missing method"
337337
// or "conversion" errors happen.
338-
func ifaceType(n ast.Expr, ti *types.Info) types.Object {
338+
func ifaceType(n ast.Expr, ti *types.Info) *types.TypeName {
339339
tv, ok := ti.Types[n]
340340
if !ok {
341341
return nil
342342
}
343343
return ifaceObjFromType(tv.Type)
344344
}
345345

346-
func ifaceObjFromType(t types.Type) types.Object {
346+
func ifaceObjFromType(t types.Type) *types.TypeName {
347347
named, ok := t.(*types.Named)
348348
if !ok {
349349
return nil

gopls/internal/lsp/source/stub.go

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"go/parser"
1414
"go/token"
1515
"go/types"
16+
"sort"
1617
"strings"
1718

1819
"golang.org/x/tools/go/analysis"
@@ -38,6 +39,13 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
3839
return nil, nil, fmt.Errorf("nil interface request")
3940
}
4041

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+
4149
// Parse the file defining the concrete type.
4250
concreteFilename := snapshot.FileSet().Position(si.Concrete.Obj().Pos()).Filename
4351
concreteFH, err := snapshot.GetFile(ctx, span.URIFromPath(concreteFilename))
@@ -56,24 +64,36 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
5664
methodsSrc = stubErr(ctx, parsedConcreteFile.File, si, snapshot)
5765
} else {
5866
methodsSrc, stubImports, err = stubMethods(ctx, parsedConcreteFile.File, si, snapshot)
67+
if err != nil {
68+
return nil, nil, fmt.Errorf("stubMethods: %w", err)
69+
}
5970
}
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+
}
6281
}
63-
nodes, _ = astutil.PathEnclosingInterval(parsedConcreteFile.File, si.Concrete.Obj().Pos(), si.Concrete.Obj().Pos())
6482
concreteSrc, err := concreteFH.Read()
6583
if err != nil {
6684
return nil, nil, fmt.Errorf("error reading concrete file source: %w", err)
6785
}
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) {
7088
return nil, nil, fmt.Errorf("insertion position is past the end of the file")
7189
}
7290
var buf bytes.Buffer
73-
buf.Write(concreteSrc[:insertPos])
91+
buf.Write(concreteSrc[:insertOffset])
7492
buf.WriteByte('\n')
7593
buf.Write(methodsSrc)
76-
buf.Write(concreteSrc[insertPos:])
94+
buf.Write(concreteSrc[insertOffset:])
95+
96+
// Re-parse it, splice in imports, pretty-print it.
7797
fset := token.NewFileSet()
7898
newF, err := parser.ParseFile(fset, parsedConcreteFile.File.Name.Name, buf.Bytes(), parser.ParseComments)
7999
if err != nil {
@@ -82,11 +102,12 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
82102
for _, imp := range stubImports {
83103
astutil.AddNamedImport(fset, newF, imp.Name, imp.Path)
84104
}
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 {
88107
return nil, nil, fmt.Errorf("format.Node: %w", err)
89108
}
109+
110+
// Return the diff.
90111
diffs := snapshot.View().Options().ComputeEdits(string(parsedConcreteFile.Src), source.String())
91112
tf := parsedConcreteFile.Mapper.TokFile
92113
var edits []analysis.TextEdit
@@ -234,21 +255,11 @@ returns
234255
},
235256
}
236257
*/
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) {
238259
iface, ok := ifaceObj.Type().Underlying().(*types.Interface)
239260
if !ok {
240261
return nil, fmt.Errorf("expected %v to be an interface but got %T", iface, ifaceObj.Type().Underlying())
241262
}
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-
252263
// Parse the imports from the file that declares the interface.
253264
ifaceFilename := snapshot.FileSet().Position(ifaceObj.Pos()).Filename
254265
ifaceFH, err := snapshot.GetFile(ctx, span.URIFromPath(ifaceFilename))
@@ -259,27 +270,55 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method
259270
if err != nil {
260271
return nil, fmt.Errorf("error parsing imports from interface file: %w", err)
261272
}
273+
262274
mi := &missingInterface{
263-
iface: iface,
275+
iface: ifaceObj,
264276
imports: ifaceFile.File.Imports,
265277
}
278+
279+
// Add all the interface methods not defined by the concrete type to mi.missing.
266280
for i := 0; i < iface.NumExplicitMethods(); i++ {
267281
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.
270284
if _, ok := visited[method.Name()]; !ok {
271285
mi.missing = append(mi.missing, method)
272286
visited[method.Name()] = struct{}{}
273287
}
274-
}
275-
if sel := concMS.Lookup(concPkg, method.Name()); sel != nil {
288+
} else {
289+
// Concrete type does have the interface method.
276290
implSig := sel.Type().(*types.Signature)
277291
ifaceSig := method.Type().(*types.Signature)
278292
if !types.Identical(ifaceSig, implSig) {
279293
return nil, fmt.Errorf("mimsatched %q function signatures:\nhave: %s\nwant: %s", method.Name(), implSig, ifaceSig)
280294
}
281295
}
282296
}
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+
283322
if len(mi.missing) > 0 {
284323
missing = append(missing, mi)
285324
}
@@ -290,7 +329,7 @@ func missingMethods(ctx context.Context, snapshot Snapshot, concMS *types.Method
290329
// that has all or some of its methods missing
291330
// from the destination concrete type
292331
type missingInterface struct {
293-
iface *types.Interface
332+
iface *types.TypeName
294333
imports []*ast.ImportSpec // the interface's import environment
295334
missing []*types.Func
296335
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package stub
2+
3+
// Regression test for Issue #56825: file corrupted by insertion of
4+
// methods after TypeSpec in a parenthesized TypeDecl.
5+
6+
import "io"
7+
8+
func newReadCloser() io.ReadCloser {
9+
return rdcloser{} //@suggestedfix("rd", "refactor.rewrite", "")
10+
}
11+
12+
type (
13+
A int
14+
rdcloser struct{}
15+
B int
16+
)
17+
18+
func _() {
19+
// Local types can't be stubbed as there's nowhere to put the methods.
20+
// The suggestedfix assertion can't express this yet. TODO(adonovan): support it.
21+
type local struct{}
22+
var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed`
23+
}
24+
25+
type (
26+
C int
27+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
-- suggestedfix_stub_typedecl_group_9_9 --
2+
package stub
3+
4+
// Regression test for Issue #56825: file corrupted by insertion of
5+
// methods after TypeSpec in a parenthesized TypeDecl.
6+
7+
import "io"
8+
9+
func newReadCloser() io.ReadCloser {
10+
return rdcloser{} //@suggestedfix("rd", "refactor.rewrite", "")
11+
}
12+
13+
type (
14+
A int
15+
rdcloser struct{}
16+
B int
17+
)
18+
19+
// Close implements io.ReadCloser
20+
func (rdcloser) Close() error {
21+
panic("unimplemented")
22+
}
23+
24+
// Read implements io.ReadCloser
25+
func (rdcloser) Read(p []byte) (n int, err error) {
26+
panic("unimplemented")
27+
}
28+
29+
func _() {
30+
// Local types can't be stubbed as there's nowhere to put the methods.
31+
// The suggestedfix assertion can't express this yet. TODO(adonovan): support it.
32+
type local struct{}
33+
var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed`
34+
}
35+
36+
type (
37+
C int
38+
)
39+

gopls/internal/lsp/testdata/summary.txt.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ FoldingRangesCount = 2
1313
FormatCount = 6
1414
ImportCount = 8
1515
SemanticTokenCount = 3
16-
SuggestedFixCount = 63
16+
SuggestedFixCount = 64
1717
FunctionExtractionCount = 27
1818
MethodExtractionCount = 6
1919
DefinitionsCount = 99

gopls/internal/lsp/testdata/summary_go1.18.txt.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ FoldingRangesCount = 2
1313
FormatCount = 6
1414
ImportCount = 8
1515
SemanticTokenCount = 3
16-
SuggestedFixCount = 69
16+
SuggestedFixCount = 70
1717
FunctionExtractionCount = 27
1818
MethodExtractionCount = 6
1919
DefinitionsCount = 110

0 commit comments

Comments
 (0)