From bd6d7e3e828752b060617536befd1076aaed7dfe Mon Sep 17 00:00:00 2001 From: xzb <2598514867@qq.com> Date: Sat, 5 Oct 2024 16:18:44 +0800 Subject: [PATCH] feature: code action to generate a stub function from a "no function f" type error --- gopls/internal/golang/codeaction.go | 10 +- gopls/internal/golang/fix.go | 4 +- gopls/internal/golang/stub.go | 367 ++++++++++++------ .../golang/stubmethods/stubmethods.go | 211 +++++++++- .../test/integration/misc/fix_test.go | 67 ++++ 5 files changed, 520 insertions(+), 139 deletions(-) diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 3c916628a1a..dd1811abc8d 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -309,13 +309,21 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error { strings.HasPrefix(msg, "cannot convert") || strings.Contains(msg, "not implement") { path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) - si := stubmethods.GetStubInfo(req.pkg.FileSet(), info, path, start) + si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, path, start) if si != nil { qf := typesutil.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg(), info) iface := types.TypeString(si.Interface.Type(), qf) msg := fmt.Sprintf("Declare missing methods of %s", iface) req.addApplyFixAction(msg, fixStubMethods, req.loc) } + } else if strings.Contains(msg, "has no field or method") { + path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end) + + si := stubmethods.GetCallStubInfo(req.pkg, info, path, start) + if si != nil { + msg := fmt.Sprintf("Declare missing methods of %s", si.Receiver.Obj().Name()) + req.addApplyFixAction(msg, fixMissingMethods, req.loc) + } } } diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 7c44aa4d273..44c04efbd3d 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -66,6 +66,7 @@ const ( fixSplitLines = "split_lines" fixJoinLines = "join_lines" fixStubMethods = "stub_methods" + fixMissingMethods = "missing_methods" ) // ApplyFix applies the specified kind of suggested fix to the given @@ -109,7 +110,8 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file fixInvertIfCondition: singleFile(invertIfCondition), fixSplitLines: singleFile(splitLines), fixJoinLines: singleFile(joinLines), - fixStubMethods: stubMethodsFixer, + fixStubMethods: stubMethodsIfaceFixer, + fixMissingMethods: stubMethodsCallFixer, } fixer, ok := fixers[fix] if !ok { diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index ca5f0055c3b..d225be9227f 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -28,28 +28,56 @@ import ( "golang.org/x/tools/internal/tokeninternal" ) -// stubMethodsFixer returns a suggested fix to declare the missing +// stubMethodsIfaceFixer returns a suggested fix to declare the missing // methods of the concrete type that is assigned to an interface type // at the cursor position. -func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { +func stubMethodsIfaceFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) - si := stubmethods.GetStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start) + si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start) if si == nil { return nil, nil, fmt.Errorf("nil interface request") } // A function-local type cannot be stubbed // since there's nowhere to put the methods. - // TODO(adonovan): move this check into GetStubInfo instead of offering a bad fix. + // TODO(adonovan): move this check into GetIfaceStubInfo instead of offering a bad fix. conc := si.Concrete.Obj() if conc.Parent() != conc.Pkg().Scope() { return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) } + return stubMethodsFixer(ctx, snapshot, pkg, si.Fset, conc, fromIface(si, conc)) +} + +// stubMethodsCallFixer returns a suggested fix to declare the missing +// methods that the user may want to generate based on CallExpr +// at the cursor position. +func stubMethodsCallFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { + nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + si := stubmethods.GetCallStubInfo(pkg, pkg.TypesInfo(), nodes, start) + if si == nil { + return nil, nil, fmt.Errorf("invalid type request") + } + + // TODO(adonovan): move this check into GetCallStubInfo instead of offering a bad fix. + recv := si.Receiver.Obj() + if recv.Parent() != recv.Pkg().Scope() { + return nil, nil, fmt.Errorf("local type %q cannot be stubbed", recv.Name()) + } + return stubMethodsFixer(ctx, snapshot, pkg, si.Fset, recv, fromCall(si, recv)) +} +func stubMethodsFixer( + ctx context.Context, + snapshot *cache.Snapshot, + pkg *cache.Package, + fset *token.FileSet, + conc *types.TypeName, + newMethodBuf func(qual func(pkg *types.Package) string) (bytes.Buffer, error), +) (*token.FileSet, *analysis.SuggestedFix, error) { // Parse the file declaring the concrete type. // // Beware: declPGF is not necessarily covered by pkg.FileSet() or si.Fset. - declPGF, _, err := parseFull(ctx, snapshot, si.Fset, conc.Pos()) + declPGF, _, err := parseFull(ctx, snapshot, fset, conc.Pos()) if err != nil { return nil, nil, fmt.Errorf("failed to parse file %q declaring implementation type: %w", declPGF.URI, err) } @@ -64,65 +92,6 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, pkg) } - // Record all direct methods of the current object - concreteFuncs := make(map[string]struct{}) - for i := 0; i < si.Concrete.NumMethods(); i++ { - concreteFuncs[si.Concrete.Method(i).Name()] = struct{}{} - } - - // Find subset of interface methods that the concrete type lacks. - ifaceType := si.Interface.Type().Underlying().(*types.Interface) - - type missingFn struct { - fn *types.Func - needSubtle string - } - - var ( - missing []missingFn - concreteStruct, isStruct = si.Concrete.Origin().Underlying().(*types.Struct) - ) - - for i := 0; i < ifaceType.NumMethods(); i++ { - imethod := ifaceType.Method(i) - cmethod, index, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) - if cmethod == nil { - missing = append(missing, missingFn{fn: imethod}) - continue - } - - if _, ok := cmethod.(*types.Var); ok { - // len(LookupFieldOrMethod.index) = 1 => conflict, >1 => shadow. - return nil, nil, fmt.Errorf("adding method %s.%s would conflict with (or shadow) existing field", - conc.Name(), imethod.Name()) - } - - if _, exist := concreteFuncs[imethod.Name()]; exist { - if !types.Identical(cmethod.Type(), imethod.Type()) { - return nil, nil, fmt.Errorf("method %s.%s already exists but has the wrong type: got %s, want %s", - conc.Name(), imethod.Name(), cmethod.Type(), imethod.Type()) - } - continue - } - - mf := missingFn{fn: imethod} - if isStruct && len(index) > 0 { - field := concreteStruct.Field(index[0]) - - fn := field.Name() - if is[*types.Pointer](field.Type()) { - fn = "*" + fn - } - - mf.needSubtle = fmt.Sprintf("// Subtle: this method shadows the method (%s).%s of %s.%s.\n", fn, imethod.Name(), si.Concrete.Obj().Name(), field.Name()) - } - - missing = append(missing, mf) - } - if len(missing) == 0 { - return nil, nil, fmt.Errorf("no missing methods found") - } - // Build import environment for the declaring file. // (typesutil.FileQualifier works only for complete // import mappings, and requires types.) @@ -193,62 +162,9 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. return name } - // Format interface name (used only in a comment). - iface := si.Interface.Name() - if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() { - iface = ipkg.Name() + "." + iface - } - - // Pointer receiver? - var star string - if si.Pointer { - star = "*" - } - - // If there are any that have named receiver, choose the first one. - // Otherwise, use lowercase for the first letter of the object. - rn := strings.ToLower(si.Concrete.Obj().Name()[0:1]) - for i := 0; i < si.Concrete.NumMethods(); i++ { - if recv := si.Concrete.Method(i).Signature().Recv(); recv.Name() != "" { - rn = recv.Name() - break - } - } - - // Check for receiver name conflicts - checkRecvName := func(tuple *types.Tuple) bool { - for i := 0; i < tuple.Len(); i++ { - if rn == tuple.At(i).Name() { - return true - } - } - return false - } - - // Format the new methods. - var newMethods bytes.Buffer - - for index := range missing { - mrn := rn + " " - sig := missing[index].fn.Signature() - if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) { - mrn = "" - } - - fmt.Fprintf(&newMethods, `// %s implements %s. -%sfunc (%s%s%s%s) %s%s { - panic("unimplemented") -} -`, - missing[index].fn.Name(), - iface, - missing[index].needSubtle, - mrn, - star, - si.Concrete.Obj().Name(), - FormatTypeParams(si.Concrete.TypeParams()), - missing[index].fn.Name(), - strings.TrimPrefix(types.TypeString(missing[index].fn.Type(), qual), "func")) + newMethods, err := newMethodBuf(qual) + if err != nil { + return nil, nil, err } // Compute insertion point for new methods: @@ -257,7 +173,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. if err != nil { return nil, nil, bug.Errorf("internal error: end position outside file bounds: %v", err) } - concOffset, err := safetoken.Offset(si.Fset.File(conc.Pos()), conc.Pos()) + concOffset, err := safetoken.Offset(fset.File(conc.Pos()), conc.Pos()) if err != nil { return nil, nil, bug.Errorf("internal error: finding type decl offset: %v", err) } @@ -281,7 +197,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. buf.Write(input[insertOffset:]) // Re-parse the file. - fset := token.NewFileSet() + fset = token.NewFileSet() newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments|parser.SkipObjectResolution) if err != nil { return nil, nil, fmt.Errorf("could not reparse file: %w", err) @@ -305,6 +221,213 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. nil } +// fromCall generate the missing method based on type info of CallExpr. +func fromCall(si *stubmethods.CallStubInfo, recv *types.TypeName) func(qual func(pkg *types.Package) string) (bytes.Buffer, error) { + return func(qual func(pkg *types.Package) string) (bytes.Buffer, error) { + // Pointer receiver? + var star string + if si.Pointer { + star = "*" + } + + // If there are any that have named receiver, choose the first one. + // Otherwise, use lowercase for the first letter of the object. + rn := strings.ToLower(si.Receiver.Obj().Name()[0:1]) + for i := 0; i < si.Receiver.NumMethods(); i++ { + if recv := si.Receiver.Method(i).Signature().Recv(); recv.Name() != "" { + rn = recv.Name() + break + } + } + + mrn := rn + " " + + // Avoid duplicated argument name + usedNames := make(map[string]int) + for i, arg := range si.Args { + name := arg.Name + if count, exists := usedNames[name]; exists { + // Name has been used before; increment the count and append it to the name + count++ + usedNames[name] = count + si.Args[i].Name = fmt.Sprintf("%s%d", name, count) + } else { + usedNames[name] = 0 + } + } + + // Avoid conflict receiver name + for _, arg := range si.Args { + name := arg.Name + if name == rn { + mrn = "" + } + } + + // Format the new methods. + var newMethods bytes.Buffer + signature := "" + for i, arg := range si.Args { + signature = signature + arg.Name + " " + types.TypeString(types.Default(arg.Typ), qual) + if i < len(si.Args)-1 { + signature = signature + ", " + } + } + + ret := "" + if len(si.Return) > 1 { + ret = "(" + } + for i, r := range si.Return { + ret = ret + " " + types.TypeString(types.Default(r), qual) + if i < len(si.Return)-1 { + ret = ret + ", " + } + } + if len(si.Return) > 1 { + ret = ret + ")" + } + + fmt.Fprintf(&newMethods, ` +func (%s%s%s%s) %s(%s) %s{ + panic("unimplemented") +} +`, + mrn, + star, + recv.Name(), + FormatTypeParams(si.Receiver.TypeParams()), + si.MethodName, + signature, + ret, + ) + return newMethods, nil + } +} + +// fromIface generate the missing method based on type info of conc's corresponding interface. +func fromIface(si *stubmethods.IfaceStubInfo, conc *types.TypeName) func(qual func(pkg *types.Package) string) (bytes.Buffer, error) { + return func(qual func(pkg *types.Package) string) (bytes.Buffer, error) { + // Record all direct methods of the current object + concreteFuncs := make(map[string]struct{}) + for i := 0; i < si.Concrete.NumMethods(); i++ { + concreteFuncs[si.Concrete.Method(i).Name()] = struct{}{} + } + + // Find subset of interface methods that the concrete type lacks. + ifaceType := si.Interface.Type().Underlying().(*types.Interface) + + type missingFn struct { + fn *types.Func + needSubtle string + } + + var ( + missing []missingFn + concreteStruct, isStruct = si.Concrete.Origin().Underlying().(*types.Struct) + ) + + for i := 0; i < ifaceType.NumMethods(); i++ { + imethod := ifaceType.Method(i) + cmethod, index, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) + if cmethod == nil { + missing = append(missing, missingFn{fn: imethod}) + continue + } + + if _, ok := cmethod.(*types.Var); ok { + // len(LookupFieldOrMethod.index) = 1 => conflict, >1 => shadow. + return bytes.Buffer{}, fmt.Errorf("adding method %s.%s would conflict with (or shadow) existing field", + conc.Name(), imethod.Name()) + } + + if _, exist := concreteFuncs[imethod.Name()]; exist { + if !types.Identical(cmethod.Type(), imethod.Type()) { + return bytes.Buffer{}, fmt.Errorf("method %s.%s already exists but has the wrong type: got %s, want %s", + conc.Name(), imethod.Name(), cmethod.Type(), imethod.Type()) + } + continue + } + + mf := missingFn{fn: imethod} + if isStruct && len(index) > 0 { + field := concreteStruct.Field(index[0]) + + fn := field.Name() + if is[*types.Pointer](field.Type()) { + fn = "*" + fn + } + + mf.needSubtle = fmt.Sprintf("// Subtle: this method shadows the method (%s).%s of %s.%s.\n", fn, imethod.Name(), si.Concrete.Obj().Name(), field.Name()) + } + + missing = append(missing, mf) + } + if len(missing) == 0 { + return bytes.Buffer{}, fmt.Errorf("no missing methods found") + } + + // Format interface name (used only in a comment). + iface := si.Interface.Name() + if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() { + iface = ipkg.Name() + "." + iface + } + + // Pointer receiver? + var star string + if si.Pointer { + star = "*" + } + + // If there are any that have named receiver, choose the first one. + // Otherwise, use lowercase for the first letter of the object. + rn := strings.ToLower(si.Concrete.Obj().Name()[0:1]) + for i := 0; i < si.Concrete.NumMethods(); i++ { + if recv := si.Concrete.Method(i).Signature().Recv(); recv.Name() != "" { + rn = recv.Name() + break + } + } + + // Check for receiver name conflicts + checkRecvName := func(tuple *types.Tuple) bool { + for i := 0; i < tuple.Len(); i++ { + if rn == tuple.At(i).Name() { + return true + } + } + return false + } + + // Format the new methods. + var newMethods bytes.Buffer + + for index := range missing { + mrn := rn + " " + sig := missing[index].fn.Signature() + if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) { + mrn = "" + } + + fmt.Fprintf(&newMethods, `// %s implements %s. +%sfunc (%s%s%s%s) %s%s { + panic("unimplemented") +} +`, + missing[index].fn.Name(), + iface, + missing[index].needSubtle, + mrn, + star, + si.Concrete.Obj().Name(), + FormatTypeParams(si.Concrete.TypeParams()), + missing[index].fn.Name(), + strings.TrimPrefix(types.TypeString(missing[index].fn.Type(), qual), "func")) + } + return newMethods, nil + } +} + // diffToTextEdits converts diff (offset-based) edits to analysis (token.Pos) form. func diffToTextEdits(tok *token.File, diffs []diff.Edit) []analysis.TextEdit { edits := make([]analysis.TextEdit, 0, len(diffs)) diff --git a/gopls/internal/golang/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go index ee7b525a6a0..bf2c0d7a745 100644 --- a/gopls/internal/golang/stubmethods/stubmethods.go +++ b/gopls/internal/golang/stubmethods/stubmethods.go @@ -12,14 +12,18 @@ import ( "go/ast" "go/token" "go/types" + "strconv" + "strings" + + "golang.org/x/tools/gopls/internal/cache" ) // TODO(adonovan): eliminate the confusing Fset parameter; only the // file name and byte offset of Concrete are needed. -// StubInfo represents a concrete type +// IfaceStubInfo represents a concrete type // that wants to stub out an interface type -type StubInfo struct { +type IfaceStubInfo struct { // Interface is the interface that the client wants to implement. // When the interface is defined, the underlying object will be a TypeName. // Note that we keep track of types.Object instead of types.Type in order @@ -33,7 +37,7 @@ type StubInfo struct { Pointer bool } -// GetStubInfo determines whether the "missing method error" +// GetIfaceStubInfo determines whether the "missing method error" // can be used to deduced what the concrete and interface types are. // // TODO(adonovan): this function (and its following 5 helpers) tries @@ -42,7 +46,7 @@ type StubInfo struct { // function call. This is essentially what the refactor/satisfy does, // more generally. Refactor to share logic, after auditing 'satisfy' // for safety on ill-typed code. -func GetStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *StubInfo { +func GetIfaceStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *IfaceStubInfo { for _, n := range path { switch n := n.(type) { case *ast.ValueSpec: @@ -70,10 +74,149 @@ func GetStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos tok return nil } +// CallStubInfo represents a missing method +// that a receiver type is about to generate +// which has “type X has no field or method Y" error +type CallStubInfo struct { + Fset *token.FileSet // the FileSet used to type-check the types below + Receiver *types.Named // the method's receiver type + Pointer bool + Args []Arg // the argument list of new methods + MethodName string + Return []types.Type +} + +type Arg struct { + Name string + Typ types.Type // the type of argument, infered from CallExpr +} + +// GetCallStubInfo extracts necessary information to generate a method definition from +// a CallExpr. +func GetCallStubInfo(pkg *cache.Package, info *types.Info, path []ast.Node, pos token.Pos) *CallStubInfo { + fset := pkg.FileSet() + for i, n := range path { + switch n := n.(type) { + case *ast.CallExpr: + s, ok := n.Fun.(*ast.SelectorExpr) + if !ok { + return nil + } + + // If recvExpr is a package name, compiler error would be + // e.g., "undefined: http.bar", thus will not hit this code path. + recvExpr := s.X + recvType, pointer := concreteType(recvExpr, info) + if recvType == nil || recvType.Obj().Pkg() == nil { + return nil + } + + var args []Arg + for i, arg := range n.Args { + typ, name, err := argInfo(arg, info, i) + if err != nil { + return nil + } + args = append(args, Arg{ + Name: name, + Typ: typ, + }) + } + + var rets []types.Type + if i < len(path)-1 { + switch parent := path[i+1].(type) { + case *ast.AssignStmt: + // Append all lhs's type + if len(parent.Rhs) == 1 { + for _, lhs := range parent.Lhs { + if t, ok := info.Types[lhs]; ok { + rets = append(rets, t.Type) + } + } + break + } + + // Lhs and Rhs counts do not match, give up + if len(parent.Lhs) != len(parent.Rhs) { + break + } + + // Append corresponding index of lhs's type + for i, rhs := range parent.Rhs { + if rhs.Pos() <= pos && pos <= rhs.End() { + left := parent.Lhs[i] + if t, ok := info.Types[left]; ok { + rets = append(rets, t.Type) + } + break + } + } + case *ast.CallExpr: + // Find argument containing pos. + argIdx := -1 + for i, callArg := range parent.Args { + if callArg.Pos() <= pos && pos <= callArg.End() { + argIdx = i + break + } + } + if argIdx == -1 { + break + } + + var def types.Object + switch f := parent.Fun.(type) { + // functon call + case *ast.Ident: + def, ok = info.Uses[f] + if !ok { + break + } + // method call + case *ast.SelectorExpr: + def, ok = info.Uses[f.Sel] + if !ok { + break + } + } + + sig, ok := types.Unalias(def.Type()).(*types.Signature) + if !ok { + break + } + var paramType types.Type + if argIdx >= sig.Params().Len() { + if sig.Variadic() { + paramType = sig.Params().At(sig.Params().Len() - 1).Type() + } else { + break + } + } else { + paramType = sig.Params().At(argIdx).Type() + } + rets = append(rets, paramType) + break + } + } + + return &CallStubInfo{ + Fset: fset, + Receiver: recvType, + MethodName: s.Sel.Name, + Pointer: pointer, + Args: args, + Return: rets, + } + } + } + return nil +} + // fromCallExpr tries to find an *ast.CallExpr's function declaration and // analyzes a function call's signature against the passed in parameter to deduce // the concrete and interface types. -func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *ast.CallExpr) *StubInfo { +func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *ast.CallExpr) *IfaceStubInfo { // Find argument containing pos. argIdx := -1 var arg ast.Expr @@ -116,7 +259,7 @@ func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *as if iface == nil { return nil } - return &StubInfo{ + return &IfaceStubInfo{ Fset: fset, Concrete: concType, Pointer: pointer, @@ -128,8 +271,8 @@ func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *as // 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(fset *token.FileSet, info *types.Info, pos token.Pos, path []ast.Node, ret *ast.ReturnStmt) (*StubInfo, error) { +// would return StubIfaceInfo with the interface being io.Writer and the concrete type being myType{}. +func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path []ast.Node, ret *ast.ReturnStmt) (*IfaceStubInfo, error) { // Find return operand containing pos. returnIdx := -1 for i, r := range ret.Results { @@ -159,7 +302,7 @@ func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path [ if iface == nil { return nil, nil } - return &StubInfo{ + return &IfaceStubInfo{ Fset: fset, Concrete: concType, Pointer: pointer, @@ -167,9 +310,9 @@ func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path [ }, nil } -// fromValueSpec returns *StubInfo from a variable declaration such as +// fromValueSpec returns *StubIfaceInfo from a variable declaration such as // var x io.Writer = &T{} -func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, pos token.Pos) *StubInfo { +func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, pos token.Pos) *IfaceStubInfo { // Find RHS element containing pos. var rhs ast.Expr for _, r := range spec.Values { @@ -197,7 +340,7 @@ func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, p if ifaceObj == nil { return nil } - return &StubInfo{ + return &IfaceStubInfo{ Fset: fset, Concrete: concType, Interface: ifaceObj, @@ -205,10 +348,10 @@ func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, p } } -// fromAssignStmt returns *StubInfo from a variable assignment such as +// fromAssignStmt returns *StubIfaceInfo from a variable assignment such as // var x io.Writer // x = &T{} -func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStmt, pos token.Pos) *StubInfo { +func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStmt, pos token.Pos) *IfaceStubInfo { // The interface conversion error in an assignment is against the RHS: // // var x io.Writer @@ -243,7 +386,7 @@ func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStm if concType == nil || concType.Obj().Pkg() == nil { return nil } - return &StubInfo{ + return &IfaceStubInfo{ Fset: fset, Concrete: concType, Interface: ifaceObj, @@ -318,3 +461,41 @@ func enclosingFunction(path []ast.Node, info *types.Info) *ast.FuncType { } return nil } + +// argInfo generate placeholder name heuristicly for a function argument. +func argInfo(e ast.Expr, info *types.Info, i int) (types.Type, string, error) { + tv, ok := info.Types[e] + if !ok { + return nil, "", fmt.Errorf("no type info") + } + + ident, ok := e.(*ast.Ident) + if ok { + // uses the identifier's name as the argument name. + return tv.Type, ident.Name, nil + } + + typ := tv.Type + ptr, isPtr := types.Unalias(typ).(*types.Pointer) + if isPtr { + typ = ptr.Elem() + } + + // Uses the first character of the type name as the argument name for builtin types + switch t := types.Default(typ).(type) { + case *types.Basic: + return tv.Type, t.Name()[0:1], nil + case *types.Signature: + return tv.Type, "f", nil + case *types.Map: + return tv.Type, "m", nil + case *types.Chan: + return tv.Type, "ch", nil + case *types.Named: + n := t.Obj().Name() + // "FooBar" becomes "fooBar" + return tv.Type, strings.ToLower(n[0:1]) + n[1:], nil + default: + return tv.Type, "args" + strconv.Itoa(i), nil + } +} diff --git a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go index 5a01afe2400..76bc976dc37 100644 --- a/gopls/internal/test/integration/misc/fix_test.go +++ b/gopls/internal/test/integration/misc/fix_test.go @@ -160,3 +160,70 @@ func _() { // yay, no panic }) } + +func TestStubMethodCallExpr(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +import "net/http" + +type Foo struct {} + +func main() { + var st string + st = "adasd" + f := Foo{} + B(f.bar(st, 3, "asd", "asdas", "asdsad", http.ErrShortBody, HelloWord(), []int{1}, http.ErrShortBody, [1]int{1}, make(chan string, 0), make(map[string]string, 0))) +} +func B(s Foo) string { return "" } +func HelloWord() Foo{ + return Foo{} +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + var d protocol.PublishDiagnosticsParams + env.AfterChange( + ReadDiagnostics("main.go", &d), + ) + + loc := env.RegexpSearch("main.go", "bar") + fixes := env.CodeAction(loc, d.Diagnostics, protocol.CodeActionUnknownTrigger) + for _, act := range fixes { + if act.Kind == protocol.QuickFix { + env.ApplyCodeAction(act) + break + } + } + + want := `package main + +import "net/http" + +type Foo struct{} + +func (f Foo) bar(st string, i int, s string, s1 string, s2 string, protocolError *http.ProtocolError, foo Foo, args7 []int, protocolError1 *http.ProtocolError, args9 [1]int, ch chan string, m map[string]string) Foo { + panic("unimplemented") +} + +func main() { + var st string + st = "adasd" + f := Foo{} + B(f.bar(st, 3, "asd", "asdas", "asdsad", http.ErrShortBody, HelloWord(), []int{1}, http.ErrShortBody, [1]int{1}, make(chan string, 0), make(map[string]string, 0))) +} +func B(s Foo) string { return "" } +func HelloWord() Foo { + return Foo{} +} +` + if got := env.BufferText("main.go"); got != want { + t.Fatalf("TestStubMethodCallExpr failed:\n%s", compare.Text(want, got)) + } + }) +}