forked from golang/tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
1a7ca93
commit d03ad85
Showing
32 changed files
with
1,725 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// Copyright 2019 The Go Authors. All rights reserved. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package stubmethods | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"go/ast" | ||
"go/format" | ||
"go/token" | ||
"go/types" | ||
"strings" | ||
|
||
"golang.org/x/tools/go/analysis" | ||
"golang.org/x/tools/go/analysis/passes/inspect" | ||
"golang.org/x/tools/go/ast/astutil" | ||
"golang.org/x/tools/internal/analysisinternal" | ||
"golang.org/x/tools/internal/lsp/lsputil" | ||
"golang.org/x/tools/internal/typesinternal" | ||
) | ||
|
||
const Doc = `stub methods analyzer | ||
This analyzer generates method stubs for concrete types | ||
in order to implement a target interface` | ||
|
||
var Analyzer = &analysis.Analyzer{ | ||
Name: "stubmethods", | ||
Doc: Doc, | ||
Requires: []*analysis.Analyzer{inspect.Analyzer}, | ||
Run: run, | ||
RunDespiteErrors: true, | ||
} | ||
|
||
func run(pass *analysis.Pass) (interface{}, error) { | ||
for _, err := range analysisinternal.GetTypeErrors(pass) { | ||
ifaceErr := strings.Contains(err.Msg, "missing method") || strings.HasPrefix(err.Msg, "cannot convert") | ||
if !ifaceErr { | ||
continue | ||
} | ||
var file *ast.File | ||
for _, f := range pass.Files { | ||
if f.Pos() <= err.Pos && err.Pos < f.End() { | ||
file = f | ||
break | ||
} | ||
} | ||
if file == nil { | ||
continue | ||
} | ||
// Get the end position of the error. | ||
_, _, endPos, ok := typesinternal.ReadGo116ErrorData(err) | ||
if !ok { | ||
var buf bytes.Buffer | ||
if err := format.Node(&buf, pass.Fset, file); err != nil { | ||
continue | ||
} | ||
endPos = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos) | ||
} | ||
path, _ := astutil.PathEnclosingInterval(file, err.Pos, endPos) | ||
si := GetStubInfo(pass.TypesInfo, path, pass.Pkg, err.Pos) | ||
if si == nil { | ||
continue | ||
} | ||
pass.Report(analysis.Diagnostic{ | ||
Pos: err.Pos, | ||
End: endPos, | ||
Message: fmt.Sprintf("Implement %s", getIfaceName(si.Concrete.Pkg(), si.Interface.Pkg(), si.Interface)), | ||
}) | ||
} | ||
return nil, nil | ||
} | ||
|
||
func getIfaceName(pkg, ifacePkg *types.Package, ifaceObj types.Object) string { | ||
return types.TypeString(ifaceObj.Type(), func(other *types.Package) string { | ||
if other == pkg { | ||
return "" | ||
} | ||
return other.Name() | ||
}) | ||
} | ||
|
||
// StubInfo represents a concrete type | ||
// that wants to stub out an interface type | ||
type StubInfo struct { | ||
Interface types.Object | ||
Concrete types.Object | ||
Pointer bool | ||
} | ||
|
||
// GetStubInfo determines whether the "missing method error" | ||
// can be used to deduced what the concrete and interface types are | ||
func GetStubInfo(ti *types.Info, path []ast.Node, pkg *types.Package, pos token.Pos) *StubInfo { | ||
for _, n := range path { | ||
switch n := n.(type) { | ||
case *ast.ValueSpec: | ||
return fromValueSpec(ti, n, pkg, pos) | ||
case *ast.ReturnStmt: | ||
si, _ := fromReturnStmt(ti, pos, path, n, pkg) | ||
return si | ||
case *ast.AssignStmt: | ||
return fromAssignStmt(ti, n, pkg, pos) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// getStubInfoFromReturns analyzes a "return" statement to extract | ||
// a concrete type that is trying to be returned as an interface type. | ||
// | ||
// For example, func() io.Writer { return myType{} } | ||
// would return StubInfo with the interface being io.Writer and the concrete type being myType{}. | ||
func fromReturnStmt(ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.ReturnStmt, pkg *types.Package) (*StubInfo, error) { | ||
returnIdx := -1 | ||
for i, r := range rs.Results { | ||
if pos >= r.Pos() && pos <= r.End() { | ||
returnIdx = i | ||
} | ||
} | ||
if returnIdx == -1 { | ||
return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, rs.Pos(), rs.End()) | ||
} | ||
n := rs.Results[returnIdx] | ||
concObj, pointer := getConcreteType(n, ti) | ||
if concObj == nil { | ||
return nil, nil | ||
} | ||
si := StubInfo{ | ||
Concrete: concObj, | ||
Pointer: pointer, | ||
} | ||
fi := lsputil.EnclosingFunction(path, ti) | ||
if fi == nil { | ||
return nil, fmt.Errorf("could not find function in a return statement") | ||
} | ||
si.Interface = getIfaceType(fi.Type.Results.List[returnIdx].Type, ti) | ||
return &si, nil | ||
} | ||
|
||
// fromValueSpec returns *StubInfo from a variable declaration such as | ||
// var x io.Writer = &T{} | ||
func fromValueSpec(ti *types.Info, vs *ast.ValueSpec, pkg *types.Package, pos token.Pos) *StubInfo { | ||
var idx int | ||
for i, vs := range vs.Values { | ||
if pos >= vs.Pos() && pos <= vs.End() { | ||
idx = i | ||
break | ||
} | ||
} | ||
|
||
valueNode := vs.Values[idx] | ||
ifaceNode := vs.Type | ||
callExp, ok := valueNode.(*ast.CallExpr) | ||
// if the ValueSpec is `var _ = myInterface(...)` | ||
// as opposed to `var _ myInterface = ...` | ||
if ifaceNode == nil && ok && len(callExp.Args) == 1 { | ||
ifaceNode = callExp.Fun | ||
valueNode = callExp.Args[0] | ||
} | ||
concreteObj, pointer := getConcreteType(valueNode, ti) | ||
if concreteObj == nil || concreteObj.Pkg() == nil { | ||
return nil | ||
} | ||
ifaceObj := getIfaceType(ifaceNode, ti) | ||
if ifaceObj == nil { | ||
return nil | ||
} | ||
return &StubInfo{ | ||
Concrete: concreteObj, | ||
Interface: ifaceObj, | ||
Pointer: pointer, | ||
} | ||
} | ||
|
||
// fromAssignStmt returns *StubInfo from a variable re-assignment such as | ||
// var x io.Writer | ||
// x = &T{} | ||
func fromAssignStmt(ti *types.Info, as *ast.AssignStmt, pkg *types.Package, pos token.Pos) *StubInfo { | ||
idx := -1 | ||
var lhs, rhs ast.Expr | ||
// Given a re-assignment interface converstion error, | ||
// the compiler error shows up on the right hand side of the expression. | ||
// For example, x = &T{} where x is io.Writer highlights the error | ||
// under "&T{}" and not "x". | ||
for i, hs := range as.Rhs { | ||
if pos >= hs.Pos() && pos <= hs.End() { | ||
idx = i | ||
break | ||
} | ||
} | ||
if idx == -1 { | ||
return nil | ||
} | ||
// Technically, this should never happen as | ||
// we would get a "cannot assign N values to M variables" | ||
// before we get an interface conversion error. Nonetheless, | ||
// guard against out of range index errors. | ||
if idx >= len(as.Lhs) { | ||
return nil | ||
} | ||
lhs, rhs = as.Lhs[idx], as.Rhs[idx] | ||
ifaceObj := getIfaceType(lhs, ti) | ||
if ifaceObj == nil { | ||
return nil | ||
} | ||
concObj, pointer := getConcreteType(rhs, ti) | ||
|
||
if concObj == nil { | ||
return nil | ||
} | ||
return &StubInfo{ | ||
Concrete: concObj, | ||
Interface: ifaceObj, | ||
Pointer: pointer, | ||
} | ||
} | ||
|
||
// getIfaceType will try to extract the types.Object that defines | ||
// the interface given the ast.Expr where the "missing method" | ||
// or "conversion" errors happen. | ||
func getIfaceType(n ast.Expr, ti *types.Info) types.Object { | ||
tv, ok := ti.Types[n] | ||
if !ok { | ||
return nil | ||
} | ||
typ := tv.Type | ||
named, ok := typ.(*types.Named) | ||
if !ok { | ||
return nil | ||
} | ||
_, ok = named.Underlying().(*types.Interface) | ||
if !ok { | ||
return nil | ||
} | ||
if named.Obj().Pkg() == nil && named.Obj().Name() != "error" { | ||
return nil | ||
} | ||
return named.Obj() | ||
} | ||
|
||
// getConcreteType will try to extract the types.Object that defines | ||
// the concrete type given the ast.Expr where the "missing method" | ||
// or "conversion" errors happened. If the concrete type is something | ||
// that cannot have methods defined on it (such as basic types), this | ||
// method will return a nil types.Object. | ||
func getConcreteType(n ast.Expr, ti *types.Info) (types.Object, bool) { | ||
tv, ok := ti.Types[n] | ||
if !ok { | ||
return nil, false | ||
} | ||
typ := tv.Type | ||
ptr, isPtr := typ.(*types.Pointer) | ||
if isPtr { | ||
typ = ptr.Elem() | ||
} | ||
nd, ok := typ.(*types.Named) | ||
if !ok { | ||
return nil, false | ||
} | ||
return nd.Obj(), isPtr | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package lsputil | ||
|
||
import ( | ||
"go/ast" | ||
"go/types" | ||
) | ||
|
||
// FuncInfo contains information about a Go function | ||
type FuncInfo struct { | ||
// Sig is the function declaration enclosing the position. | ||
Sig *types.Signature | ||
|
||
// Body is the function's Body. | ||
Body *ast.BlockStmt | ||
|
||
// Type carries the AST representation of Sig | ||
Type *ast.FuncType | ||
} | ||
|
||
// EnclosingFunction returns the signature and body of the function | ||
// enclosing the given position. | ||
func EnclosingFunction(path []ast.Node, info *types.Info) *FuncInfo { | ||
for _, node := range path { | ||
switch t := node.(type) { | ||
case *ast.FuncDecl: | ||
if obj, ok := info.Defs[t.Name]; ok { | ||
return &FuncInfo{ | ||
Sig: obj.Type().(*types.Signature), | ||
Body: t.Body, | ||
Type: t.Type, | ||
} | ||
} | ||
case *ast.FuncLit: | ||
if typ, ok := info.Types[t]; ok { | ||
return &FuncInfo{ | ||
Sig: typ.Type.(*types.Signature), | ||
Body: t.Body, | ||
Type: t.Type, | ||
} | ||
} | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.