Skip to content

Commit

Permalink
gopls,internal/lsp: Implement method stubbing via CodeAction
Browse files Browse the repository at this point in the history
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
marwan-at-work committed Sep 24, 2021
1 parent 1a7ca93 commit d03ad85
Show file tree
Hide file tree
Showing 32 changed files with 1,725 additions and 42 deletions.
263 changes: 263 additions & 0 deletions internal/lsp/analysis/stubmethods/stubmethods.go
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
}
44 changes: 44 additions & 0 deletions internal/lsp/lsputil/lsputil.go
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
}
40 changes: 4 additions & 36 deletions internal/lsp/source/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/fuzzy"
"golang.org/x/tools/internal/lsp/lsputil"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/lsp/source"
Expand Down Expand Up @@ -198,7 +199,7 @@ type completer struct {

// enclosingFunc contains information about the function enclosing
// the position.
enclosingFunc *funcInfo
enclosingFunc *lsputil.FuncInfo

// enclosingCompositeLiteral contains information about the composite literal
// enclosing the position.
Expand All @@ -223,15 +224,6 @@ type completer struct {
startTime time.Time
}

// funcInfo holds info about a function object.
type funcInfo struct {
// sig is the function declaration enclosing the position.
sig *types.Signature

// body is the function's body.
body *ast.BlockStmt
}

type compLitInfo struct {
// cl is the *ast.CompositeLit enclosing the position.
cl *ast.CompositeLit
Expand Down Expand Up @@ -505,7 +497,7 @@ func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHan
path: path,
pos: pos,
seen: make(map[types.Object]bool),
enclosingFunc: enclosingFunction(path, pkg.GetTypesInfo()),
enclosingFunc: lsputil.EnclosingFunction(path, pkg.GetTypesInfo()),
enclosingCompositeLiteral: enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo()),
deepState: deepCompletionState{
enabled: opts.DeepCompletion,
Expand Down Expand Up @@ -1680,30 +1672,6 @@ func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info)
return nil
}

// 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,
}
}
case *ast.FuncLit:
if typ, ok := info.Types[t]; ok {
return &funcInfo{
sig: typ.Type.(*types.Signature),
body: t.Body,
}
}
}
}
return nil
}

func (c *completer) expectedCompositeLiteralType() types.Type {
clInfo := c.enclosingCompositeLiteral
switch t := clInfo.clType.(type) {
Expand Down Expand Up @@ -2027,7 +1995,7 @@ Nodes:
}
case *ast.ReturnStmt:
if c.enclosingFunc != nil {
sig := c.enclosingFunc.sig
sig := c.enclosingFunc.Sig
// Find signature result that corresponds to our return statement.
if resultIdx := exprAtPos(c.pos, node.Results); resultIdx < len(node.Results) {
if resultIdx < sig.Results().Len() {
Expand Down
2 changes: 1 addition & 1 deletion internal/lsp/source/completion/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (c *completer) labels(lt labelType) {
// Goto accepts any label in the same function not in a nested
// block. It also doesn't take labels that would jump across
// variable definitions, but ignore that case for now.
ast.Inspect(c.enclosingFunc.body, func(n ast.Node) bool {
ast.Inspect(c.enclosingFunc.Body, func(n ast.Node) bool {
if n == nil {
return false
}
Expand Down
Loading

0 comments on commit d03ad85

Please sign in to comment.