Skip to content

Commit d03ad85

Browse files
gopls,internal/lsp: Implement method stubbing via CodeAction
This CL adds a quickfix CodeAction that detects "missing method" compiler errors and suggests adding method stubs to the concrete type that would implement the interface. There are many ways that a user might indicate a concrete type is meant to be used as an interface. This PR detects two types of those errors: variable declaration and function returns. For variable declarations, things like the following should be detected: 1. var _ SomeInterface = SomeType{} 2. var _ = SomeInterface(SomeType{}) 3. var _ SomeInterface = (*SomeType)(nil) For function returns, the following example is the primary detection: func newIface() SomeInterface { return &SomeType{} } More detections can be added in the future of course. Fixes golang/go#37537 Change-Id: Ibb7784622184c9885eff2ccc786767682876b4d3
1 parent 1a7ca93 commit d03ad85

32 files changed

+1725
-42
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package stubmethods
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"go/ast"
11+
"go/format"
12+
"go/token"
13+
"go/types"
14+
"strings"
15+
16+
"golang.org/x/tools/go/analysis"
17+
"golang.org/x/tools/go/analysis/passes/inspect"
18+
"golang.org/x/tools/go/ast/astutil"
19+
"golang.org/x/tools/internal/analysisinternal"
20+
"golang.org/x/tools/internal/lsp/lsputil"
21+
"golang.org/x/tools/internal/typesinternal"
22+
)
23+
24+
const Doc = `stub methods analyzer
25+
26+
This analyzer generates method stubs for concrete types
27+
in order to implement a target interface`
28+
29+
var Analyzer = &analysis.Analyzer{
30+
Name: "stubmethods",
31+
Doc: Doc,
32+
Requires: []*analysis.Analyzer{inspect.Analyzer},
33+
Run: run,
34+
RunDespiteErrors: true,
35+
}
36+
37+
func run(pass *analysis.Pass) (interface{}, error) {
38+
for _, err := range analysisinternal.GetTypeErrors(pass) {
39+
ifaceErr := strings.Contains(err.Msg, "missing method") || strings.HasPrefix(err.Msg, "cannot convert")
40+
if !ifaceErr {
41+
continue
42+
}
43+
var file *ast.File
44+
for _, f := range pass.Files {
45+
if f.Pos() <= err.Pos && err.Pos < f.End() {
46+
file = f
47+
break
48+
}
49+
}
50+
if file == nil {
51+
continue
52+
}
53+
// Get the end position of the error.
54+
_, _, endPos, ok := typesinternal.ReadGo116ErrorData(err)
55+
if !ok {
56+
var buf bytes.Buffer
57+
if err := format.Node(&buf, pass.Fset, file); err != nil {
58+
continue
59+
}
60+
endPos = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos)
61+
}
62+
path, _ := astutil.PathEnclosingInterval(file, err.Pos, endPos)
63+
si := GetStubInfo(pass.TypesInfo, path, pass.Pkg, err.Pos)
64+
if si == nil {
65+
continue
66+
}
67+
pass.Report(analysis.Diagnostic{
68+
Pos: err.Pos,
69+
End: endPos,
70+
Message: fmt.Sprintf("Implement %s", getIfaceName(si.Concrete.Pkg(), si.Interface.Pkg(), si.Interface)),
71+
})
72+
}
73+
return nil, nil
74+
}
75+
76+
func getIfaceName(pkg, ifacePkg *types.Package, ifaceObj types.Object) string {
77+
return types.TypeString(ifaceObj.Type(), func(other *types.Package) string {
78+
if other == pkg {
79+
return ""
80+
}
81+
return other.Name()
82+
})
83+
}
84+
85+
// StubInfo represents a concrete type
86+
// that wants to stub out an interface type
87+
type StubInfo struct {
88+
Interface types.Object
89+
Concrete types.Object
90+
Pointer bool
91+
}
92+
93+
// GetStubInfo determines whether the "missing method error"
94+
// can be used to deduced what the concrete and interface types are
95+
func GetStubInfo(ti *types.Info, path []ast.Node, pkg *types.Package, pos token.Pos) *StubInfo {
96+
for _, n := range path {
97+
switch n := n.(type) {
98+
case *ast.ValueSpec:
99+
return fromValueSpec(ti, n, pkg, pos)
100+
case *ast.ReturnStmt:
101+
si, _ := fromReturnStmt(ti, pos, path, n, pkg)
102+
return si
103+
case *ast.AssignStmt:
104+
return fromAssignStmt(ti, n, pkg, pos)
105+
}
106+
}
107+
return nil
108+
}
109+
110+
// getStubInfoFromReturns analyzes a "return" statement to extract
111+
// a concrete type that is trying to be returned as an interface type.
112+
//
113+
// For example, func() io.Writer { return myType{} }
114+
// would return StubInfo with the interface being io.Writer and the concrete type being myType{}.
115+
func fromReturnStmt(ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.ReturnStmt, pkg *types.Package) (*StubInfo, error) {
116+
returnIdx := -1
117+
for i, r := range rs.Results {
118+
if pos >= r.Pos() && pos <= r.End() {
119+
returnIdx = i
120+
}
121+
}
122+
if returnIdx == -1 {
123+
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, rs.Pos(), rs.End())
124+
}
125+
n := rs.Results[returnIdx]
126+
concObj, pointer := getConcreteType(n, ti)
127+
if concObj == nil {
128+
return nil, nil
129+
}
130+
si := StubInfo{
131+
Concrete: concObj,
132+
Pointer: pointer,
133+
}
134+
fi := lsputil.EnclosingFunction(path, ti)
135+
if fi == nil {
136+
return nil, fmt.Errorf("could not find function in a return statement")
137+
}
138+
si.Interface = getIfaceType(fi.Type.Results.List[returnIdx].Type, ti)
139+
return &si, nil
140+
}
141+
142+
// fromValueSpec returns *StubInfo from a variable declaration such as
143+
// var x io.Writer = &T{}
144+
func fromValueSpec(ti *types.Info, vs *ast.ValueSpec, pkg *types.Package, pos token.Pos) *StubInfo {
145+
var idx int
146+
for i, vs := range vs.Values {
147+
if pos >= vs.Pos() && pos <= vs.End() {
148+
idx = i
149+
break
150+
}
151+
}
152+
153+
valueNode := vs.Values[idx]
154+
ifaceNode := vs.Type
155+
callExp, ok := valueNode.(*ast.CallExpr)
156+
// if the ValueSpec is `var _ = myInterface(...)`
157+
// as opposed to `var _ myInterface = ...`
158+
if ifaceNode == nil && ok && len(callExp.Args) == 1 {
159+
ifaceNode = callExp.Fun
160+
valueNode = callExp.Args[0]
161+
}
162+
concreteObj, pointer := getConcreteType(valueNode, ti)
163+
if concreteObj == nil || concreteObj.Pkg() == nil {
164+
return nil
165+
}
166+
ifaceObj := getIfaceType(ifaceNode, ti)
167+
if ifaceObj == nil {
168+
return nil
169+
}
170+
return &StubInfo{
171+
Concrete: concreteObj,
172+
Interface: ifaceObj,
173+
Pointer: pointer,
174+
}
175+
}
176+
177+
// fromAssignStmt returns *StubInfo from a variable re-assignment such as
178+
// var x io.Writer
179+
// x = &T{}
180+
func fromAssignStmt(ti *types.Info, as *ast.AssignStmt, pkg *types.Package, pos token.Pos) *StubInfo {
181+
idx := -1
182+
var lhs, rhs ast.Expr
183+
// Given a re-assignment interface converstion error,
184+
// the compiler error shows up on the right hand side of the expression.
185+
// For example, x = &T{} where x is io.Writer highlights the error
186+
// under "&T{}" and not "x".
187+
for i, hs := range as.Rhs {
188+
if pos >= hs.Pos() && pos <= hs.End() {
189+
idx = i
190+
break
191+
}
192+
}
193+
if idx == -1 {
194+
return nil
195+
}
196+
// Technically, this should never happen as
197+
// we would get a "cannot assign N values to M variables"
198+
// before we get an interface conversion error. Nonetheless,
199+
// guard against out of range index errors.
200+
if idx >= len(as.Lhs) {
201+
return nil
202+
}
203+
lhs, rhs = as.Lhs[idx], as.Rhs[idx]
204+
ifaceObj := getIfaceType(lhs, ti)
205+
if ifaceObj == nil {
206+
return nil
207+
}
208+
concObj, pointer := getConcreteType(rhs, ti)
209+
210+
if concObj == nil {
211+
return nil
212+
}
213+
return &StubInfo{
214+
Concrete: concObj,
215+
Interface: ifaceObj,
216+
Pointer: pointer,
217+
}
218+
}
219+
220+
// getIfaceType will try to extract the types.Object that defines
221+
// the interface given the ast.Expr where the "missing method"
222+
// or "conversion" errors happen.
223+
func getIfaceType(n ast.Expr, ti *types.Info) types.Object {
224+
tv, ok := ti.Types[n]
225+
if !ok {
226+
return nil
227+
}
228+
typ := tv.Type
229+
named, ok := typ.(*types.Named)
230+
if !ok {
231+
return nil
232+
}
233+
_, ok = named.Underlying().(*types.Interface)
234+
if !ok {
235+
return nil
236+
}
237+
if named.Obj().Pkg() == nil && named.Obj().Name() != "error" {
238+
return nil
239+
}
240+
return named.Obj()
241+
}
242+
243+
// getConcreteType will try to extract the types.Object that defines
244+
// the concrete type given the ast.Expr where the "missing method"
245+
// or "conversion" errors happened. If the concrete type is something
246+
// that cannot have methods defined on it (such as basic types), this
247+
// method will return a nil types.Object.
248+
func getConcreteType(n ast.Expr, ti *types.Info) (types.Object, bool) {
249+
tv, ok := ti.Types[n]
250+
if !ok {
251+
return nil, false
252+
}
253+
typ := tv.Type
254+
ptr, isPtr := typ.(*types.Pointer)
255+
if isPtr {
256+
typ = ptr.Elem()
257+
}
258+
nd, ok := typ.(*types.Named)
259+
if !ok {
260+
return nil, false
261+
}
262+
return nd.Obj(), isPtr
263+
}

internal/lsp/lsputil/lsputil.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package lsputil
2+
3+
import (
4+
"go/ast"
5+
"go/types"
6+
)
7+
8+
// FuncInfo contains information about a Go function
9+
type FuncInfo struct {
10+
// Sig is the function declaration enclosing the position.
11+
Sig *types.Signature
12+
13+
// Body is the function's Body.
14+
Body *ast.BlockStmt
15+
16+
// Type carries the AST representation of Sig
17+
Type *ast.FuncType
18+
}
19+
20+
// EnclosingFunction returns the signature and body of the function
21+
// enclosing the given position.
22+
func EnclosingFunction(path []ast.Node, info *types.Info) *FuncInfo {
23+
for _, node := range path {
24+
switch t := node.(type) {
25+
case *ast.FuncDecl:
26+
if obj, ok := info.Defs[t.Name]; ok {
27+
return &FuncInfo{
28+
Sig: obj.Type().(*types.Signature),
29+
Body: t.Body,
30+
Type: t.Type,
31+
}
32+
}
33+
case *ast.FuncLit:
34+
if typ, ok := info.Types[t]; ok {
35+
return &FuncInfo{
36+
Sig: typ.Type.(*types.Signature),
37+
Body: t.Body,
38+
Type: t.Type,
39+
}
40+
}
41+
}
42+
}
43+
return nil
44+
}

internal/lsp/source/completion/completion.go

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"golang.org/x/tools/internal/event"
2727
"golang.org/x/tools/internal/imports"
2828
"golang.org/x/tools/internal/lsp/fuzzy"
29+
"golang.org/x/tools/internal/lsp/lsputil"
2930
"golang.org/x/tools/internal/lsp/protocol"
3031
"golang.org/x/tools/internal/lsp/snippet"
3132
"golang.org/x/tools/internal/lsp/source"
@@ -198,7 +199,7 @@ type completer struct {
198199

199200
// enclosingFunc contains information about the function enclosing
200201
// the position.
201-
enclosingFunc *funcInfo
202+
enclosingFunc *lsputil.FuncInfo
202203

203204
// enclosingCompositeLiteral contains information about the composite literal
204205
// enclosing the position.
@@ -223,15 +224,6 @@ type completer struct {
223224
startTime time.Time
224225
}
225226

226-
// funcInfo holds info about a function object.
227-
type funcInfo struct {
228-
// sig is the function declaration enclosing the position.
229-
sig *types.Signature
230-
231-
// body is the function's body.
232-
body *ast.BlockStmt
233-
}
234-
235227
type compLitInfo struct {
236228
// cl is the *ast.CompositeLit enclosing the position.
237229
cl *ast.CompositeLit
@@ -505,7 +497,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
505497
path: path,
506498
pos: pos,
507499
seen: make(map[types.Object]bool),
508-
enclosingFunc: enclosingFunction(path, pkg.GetTypesInfo()),
500+
enclosingFunc: lsputil.EnclosingFunction(path, pkg.GetTypesInfo()),
509501
enclosingCompositeLiteral: enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo()),
510502
deepState: deepCompletionState{
511503
enabled: opts.DeepCompletion,
@@ -1680,30 +1672,6 @@ func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info)
16801672
return nil
16811673
}
16821674

1683-
// enclosingFunction returns the signature and body of the function
1684-
// enclosing the given position.
1685-
func enclosingFunction(path []ast.Node, info *types.Info) *funcInfo {
1686-
for _, node := range path {
1687-
switch t := node.(type) {
1688-
case *ast.FuncDecl:
1689-
if obj, ok := info.Defs[t.Name]; ok {
1690-
return &funcInfo{
1691-
sig: obj.Type().(*types.Signature),
1692-
body: t.Body,
1693-
}
1694-
}
1695-
case *ast.FuncLit:
1696-
if typ, ok := info.Types[t]; ok {
1697-
return &funcInfo{
1698-
sig: typ.Type.(*types.Signature),
1699-
body: t.Body,
1700-
}
1701-
}
1702-
}
1703-
}
1704-
return nil
1705-
}
1706-
17071675
func (c *completer) expectedCompositeLiteralType() types.Type {
17081676
clInfo := c.enclosingCompositeLiteral
17091677
switch t := clInfo.clType.(type) {
@@ -2027,7 +1995,7 @@ Nodes:
20271995
}
20281996
case *ast.ReturnStmt:
20291997
if c.enclosingFunc != nil {
2030-
sig := c.enclosingFunc.sig
1998+
sig := c.enclosingFunc.Sig
20311999
// Find signature result that corresponds to our return statement.
20322000
if resultIdx := exprAtPos(c.pos, node.Results); resultIdx < len(node.Results) {
20332001
if resultIdx < sig.Results().Len() {

internal/lsp/source/completion/labels.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (c *completer) labels(lt labelType) {
8787
// Goto accepts any label in the same function not in a nested
8888
// block. It also doesn't take labels that would jump across
8989
// variable definitions, but ignore that case for now.
90-
ast.Inspect(c.enclosingFunc.body, func(n ast.Node) bool {
90+
ast.Inspect(c.enclosingFunc.Body, func(n ast.Node) bool {
9191
if n == nil {
9292
return false
9393
}

0 commit comments

Comments
 (0)