internal/lsp/source: support stubbing concrete type params

This CL adds support for generating method stubs for named types
that have type parameters and want to implement an interface.
See internal/lsp/testdata/stub/stub_generic_receiver.go for an example.
Note, this CL does not yet support type params on interface declarations.

Updates golang/go#37537

Change-Id: I2a2a18d364b2b489e2fbd8a74dfed88ae32d83b5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/389654
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Trust: Suzy Mueller <suzmue@golang.org>
This commit is contained in:
Marwan Sulaiman 2022-03-03 08:05:49 -05:00 committed by Robert Findley
parent 3286927895
commit 3ce772872c
7 changed files with 75 additions and 34 deletions

View File

@ -60,7 +60,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
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)
si := GetStubInfo(pass.TypesInfo, path, err.Pos)
if si == nil {
continue
}
@ -91,20 +91,20 @@ type StubInfo struct {
// 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 {
func GetStubInfo(ti *types.Info, path []ast.Node, pos token.Pos) *StubInfo {
for _, n := range path {
switch n := n.(type) {
case *ast.ValueSpec:
return fromValueSpec(ti, n, pkg, pos)
return fromValueSpec(ti, n, pos)
case *ast.ReturnStmt:
// An error here may not indicate a real error the user should know about, but it may.
// Therefore, it would be best to log it out for debugging/reporting purposes instead of ignoring
// it. However, event.Log takes a context which is not passed via the analysis package.
// TODO(marwan-at-work): properly log this error.
si, _ := fromReturnStmt(ti, pos, path, n, pkg)
si, _ := fromReturnStmt(ti, pos, path, n)
return si
case *ast.AssignStmt:
return fromAssignStmt(ti, n, pkg, pos)
return fromAssignStmt(ti, n, pos)
}
}
return nil
@ -115,7 +115,7 @@ func GetStubInfo(ti *types.Info, path []ast.Node, pkg *types.Package, pos token.
//
// 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) {
func fromReturnStmt(ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.ReturnStmt) (*StubInfo, error) {
returnIdx := -1
for i, r := range rs.Results {
if pos >= r.Pos() && pos <= r.End() {
@ -146,7 +146,7 @@ func fromReturnStmt(ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.Retu
// 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 {
func fromValueSpec(ti *types.Info, vs *ast.ValueSpec, pos token.Pos) *StubInfo {
var idx int
for i, vs := range vs.Values {
if pos >= vs.Pos() && pos <= vs.End() {
@ -182,7 +182,7 @@ func fromValueSpec(ti *types.Info, vs *ast.ValueSpec, pkg *types.Package, pos to
// 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 {
func fromAssignStmt(ti *types.Info, as *ast.AssignStmt, pos token.Pos) *StubInfo {
idx := -1
var lhs, rhs ast.Expr
// Given a re-assignment interface conversion error,

View File

@ -5,7 +5,6 @@
package completion
import (
"bytes"
"context"
"fmt"
"go/ast"
@ -65,7 +64,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e
x := cand.obj.(*types.TypeName)
if named, ok := x.Type().(*types.Named); ok {
tp := typeparams.ForNamed(named)
label += string(formatTypeParams(tp))
label += source.FormatTypeParams(tp)
insert = label // maintain invariant above (label == insert)
}
}
@ -339,19 +338,3 @@ func (c *completer) wantTypeParams() bool {
}
return false
}
func formatTypeParams(tp *typeparams.TypeParamList) []byte {
var buf bytes.Buffer
if tp == nil || tp.Len() == 0 {
return nil
}
buf.WriteByte('[')
for i := 0; i < tp.Len(); i++ {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(tp.At(i).Obj().Name())
}
buf.WriteByte(']')
return buf.Bytes()
}

View File

@ -20,6 +20,7 @@ import (
"golang.org/x/tools/internal/lsp/analysis/stubmethods"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/typeparams"
)
func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, rng protocol.Range) (*analysis.SuggestedFix, error) {
@ -31,7 +32,7 @@ func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh VersionedFi
if err != nil {
return nil, fmt.Errorf("getNodes: %w", err)
}
si := stubmethods.GetStubInfo(pkg.GetTypesInfo(), nodes, pkg.GetTypes(), pos)
si := stubmethods.GetStubInfo(pkg.GetTypesInfo(), nodes, pos)
if si == nil {
return nil, fmt.Errorf("nil interface request")
}
@ -134,7 +135,7 @@ func stubMethods(ctx context.Context, concreteFile *ast.File, si *stubmethods.St
_, err = methodsBuffer.Write(printStubMethod(methodData{
Method: m.Name(),
Concrete: getStubReceiver(si),
Interface: deduceIfaceName(si.Concrete.Obj().Pkg(), si.Interface.Pkg(), si.Concrete.Obj(), si.Interface),
Interface: deduceIfaceName(si.Concrete.Obj().Pkg(), si.Interface.Pkg(), si.Interface),
Signature: strings.TrimPrefix(sig, "func"),
}))
if err != nil {
@ -159,13 +160,14 @@ func stubErr(ctx context.Context, concreteFile *ast.File, si *stubmethods.StubIn
}
// getStubReceiver returns the concrete type's name as a method receiver.
// TODO(marwan-at-work): add type parameters to the receiver when the concrete type
// is a generic one.
// It accounts for type parameters if they exist.
func getStubReceiver(si *stubmethods.StubInfo) string {
concrete := si.Concrete.Obj().Name()
var concrete string
if si.Pointer {
concrete = "*" + concrete
concrete += "*"
}
concrete += si.Concrete.Obj().Name()
concrete += FormatTypeParams(typeparams.ForNamed(si.Concrete))
return concrete
}
@ -203,7 +205,7 @@ func deducePkgFromTypes(ctx context.Context, snapshot Snapshot, ifaceObj types.O
return nil, fmt.Errorf("pkg %q not found", ifaceObj.Pkg().Path())
}
func deduceIfaceName(concretePkg, ifacePkg *types.Package, concreteObj, ifaceObj types.Object) string {
func deduceIfaceName(concretePkg, ifacePkg *types.Package, ifaceObj types.Object) string {
if concretePkg.Path() == ifacePkg.Path() {
return ifaceObj.Name()
}

View File

@ -168,6 +168,25 @@ func formatFieldList(ctx context.Context, snapshot Snapshot, list *ast.FieldList
return result, writeResultParens
}
// FormatTypeParams turns TypeParamList into its Go representation, such as:
// [T, Y]. Note that it does not print constraints as this is mainly used for
// formatting type params in method receivers.
func FormatTypeParams(tp *typeparams.TypeParamList) string {
if tp == nil || tp.Len() == 0 {
return ""
}
var buf bytes.Buffer
buf.WriteByte('[')
for i := 0; i < tp.Len(); i++ {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(tp.At(i).Obj().Name())
}
buf.WriteByte(']')
return buf.String()
}
// NewSignature returns formatted signature for a types.Signature struct.
func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier) *signature {
params := make([]string, 0, sig.Params().Len())

View File

@ -0,0 +1,15 @@
//go:build go1.18
// +build go1.18
package stub
import "io"
// This file tests that that the stub method generator accounts for concrete
// types that have type parameters defined.
var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite")
type genReader[T, Y any] struct {
T T
Y Y
}

View File

@ -0,0 +1,22 @@
-- suggestedfix_stub_generic_receiver_10_23 --
//go:build go1.18
// +build go1.18
package stub
import "io"
// This file tests that that the stub method generator accounts for concrete
// types that have type parameters defined.
var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "refactor.rewrite")
type genReader[T, Y any] struct {
T T
Y Y
}
// ReadFrom implements io.ReaderFrom
func (*genReader[T, Y]) ReadFrom(r io.Reader) (n int64, err error) {
panic("unimplemented")
}

View File

@ -13,7 +13,7 @@ FoldingRangesCount = 2
FormatCount = 6
ImportCount = 8
SemanticTokenCount = 3
SuggestedFixCount = 61
SuggestedFixCount = 62
FunctionExtractionCount = 25
MethodExtractionCount = 6
DefinitionsCount = 108