diff options
author | Dan Willemsen <dwillemsen@google.com> | 2023-03-15 13:19:36 -0400 |
---|---|---|
committer | Dan Willemsen <dwillemsen@google.com> | 2023-03-15 14:18:08 -0400 |
commit | 09c5a32afc5b66f28f166a68afe1fc71afbf9b73 (patch) | |
tree | 194d7b0e539d014393564a256bec571e18d6533a /gopls/internal/lsp/source/stub.go | |
parent | f10932f763d058b0dcb3acfb795c869996fef47b (diff) | |
parent | 031fc75960d487b0b15db12fb328676236a3a39c (diff) | |
download | golang-x-tools-09c5a32afc5b66f28f166a68afe1fc71afbf9b73.tar.gz |
Not using external_updater this time to switch to the new upstream tags.
Test: treehugger
Change-Id: I31488b4958a366ed7f183bb387d3e1446acc13ae
Diffstat (limited to 'gopls/internal/lsp/source/stub.go')
-rw-r--r-- | gopls/internal/lsp/source/stub.go | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go new file mode 100644 index 000000000..6bbc1dba2 --- /dev/null +++ b/gopls/internal/lsp/source/stub.go @@ -0,0 +1,238 @@ +// Copyright 2022 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 source + +import ( + "bytes" + "context" + "fmt" + "go/format" + "go/parser" + "go/token" + "go/types" + "io" + "path" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/typeparams" +) + +// stubSuggestedFixFunc 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 stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { + pkg, pgf, err := PackageForFile(ctx, snapshot, fh.URI(), NarrowestPackage) + if err != nil { + return nil, nil, fmt.Errorf("GetTypedFile: %w", err) + } + start, end, err := pgf.RangePos(rng) + if err != nil { + return nil, nil, err + } + nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + si := stubmethods.GetStubInfo(pkg.FileSet(), pkg.GetTypesInfo(), nodes, start) + if si == nil { + return nil, nil, fmt.Errorf("nil interface request") + } + return stub(ctx, snapshot, si) +} + +// stub returns a suggested fix to declare the missing methods of si.Concrete. +func stub(ctx context.Context, snapshot Snapshot, si *stubmethods.StubInfo) (*token.FileSet, *analysis.SuggestedFix, error) { + // A function-local type cannot be stubbed + // since there's nowhere to put the methods. + conc := si.Concrete.Obj() + if conc.Parent() != conc.Pkg().Scope() { + return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) + } + + // Parse the file declaring the concrete type. + declPGF, _, err := parseFull(ctx, snapshot, si.Fset, conc.Pos()) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse file %q declaring implementation type: %w", declPGF.URI, err) + } + if declPGF.Fixed { + return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI) + } + + // Build import environment for the declaring file. + importEnv := make(map[ImportPath]string) // value is local name + for _, imp := range declPGF.File.Imports { + importPath := UnquoteImportPath(imp) + var name string + if imp.Name != nil { + name = imp.Name.Name + if name == "_" { + continue + } else if name == "." { + name = "" // see types.Qualifier + } + } else { + // TODO(adonovan): may omit a vendor/ prefix; consult the Metadata. + name = path.Base(string(importPath)) + } + importEnv[importPath] = name // latest alias wins + } + + // Find subset of interface methods that the concrete type lacks. + var missing []*types.Func + ifaceType := si.Interface.Type().Underlying().(*types.Interface) + for i := 0; i < ifaceType.NumMethods(); i++ { + imethod := ifaceType.Method(i) + cmethod, _, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) + if cmethod == nil { + missing = append(missing, 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 !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()) + } + } + if len(missing) == 0 { + return nil, nil, fmt.Errorf("no missing methods found") + } + + // Create a package name qualifier that uses the + // locally appropriate imported package name. + // It records any needed new imports. + // TODO(adonovan): factor with source.FormatVarType, stubmethods.RelativeToFiles? + // + // Prior to CL 469155 this logic preserved any renaming + // imports from the file that declares the interface + // method--ostensibly the preferred name for imports of + // frequently renamed packages such as protobufs. + // Now we use the package's declared name. If this turns out + // to be a mistake, then use parseHeader(si.iface.Pos()). + // + type newImport struct{ name, importPath string } + var newImports []newImport // for AddNamedImport + qual := func(pkg *types.Package) string { + // TODO(adonovan): don't ignore vendor prefix. + importPath := ImportPath(pkg.Path()) + name, ok := importEnv[importPath] + if !ok { + // Insert new import using package's declared name. + // + // TODO(adonovan): resolve conflict between declared + // name and existing file-level (declPGF.File.Imports) + // or package-level (si.Concrete.Pkg.Scope) decls by + // generating a fresh name. + name = pkg.Name() + importEnv[importPath] = name + new := newImport{importPath: string(importPath)} + // For clarity, use a renaming import whenever the + // local name does not match the path's last segment. + if name != path.Base(new.importPath) { + new.name = name + } + newImports = append(newImports, new) + } + 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 = "*" + } + + // Format the new methods. + var newMethods bytes.Buffer + for _, method := range missing { + fmt.Fprintf(&newMethods, `// %s implements %s +func (%s%s%s) %s%s { + panic("unimplemented") +} +`, + method.Name(), + iface, + star, + si.Concrete.Obj().Name(), + FormatTypeParams(typeparams.ForNamed(si.Concrete)), + method.Name(), + strings.TrimPrefix(types.TypeString(method.Type(), qual), "func")) + } + + // Compute insertion point for new methods: + // after the top-level declaration enclosing the (package-level) type. + insertOffset, err := safetoken.Offset(declPGF.Tok, declPGF.File.End()) + 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()) + if err != nil { + return nil, nil, bug.Errorf("internal error: finding type decl offset: %v", err) + } + for _, decl := range declPGF.File.Decls { + declEndOffset, err := safetoken.Offset(declPGF.Tok, decl.End()) + if err != nil { + return nil, nil, bug.Errorf("internal error: finding decl offset: %v", err) + } + if declEndOffset > concOffset { + insertOffset = declEndOffset + break + } + } + + // Splice the new methods into the file content. + var buf bytes.Buffer + input := declPGF.Mapper.Content // unfixed content of file + buf.Write(input[:insertOffset]) + buf.WriteByte('\n') + io.Copy(&buf, &newMethods) + buf.Write(input[insertOffset:]) + + // Re-parse the file. + fset := token.NewFileSet() + newF, err := parser.ParseFile(fset, declPGF.File.Name.Name, buf.Bytes(), parser.ParseComments) + if err != nil { + return nil, nil, fmt.Errorf("could not reparse file: %w", err) + } + + // Splice the new imports into the syntax tree. + for _, imp := range newImports { + astutil.AddNamedImport(fset, newF, imp.name, imp.importPath) + } + + // Pretty-print. + var output strings.Builder + if err := format.Node(&output, fset, newF); err != nil { + return nil, nil, fmt.Errorf("format.Node: %w", err) + } + + // Report the diff. + diffs := snapshot.View().Options().ComputeEdits(string(input), output.String()) + var edits []analysis.TextEdit + for _, edit := range diffs { + edits = append(edits, analysis.TextEdit{ + Pos: declPGF.Tok.Pos(edit.Start), + End: declPGF.Tok.Pos(edit.End), + NewText: []byte(edit.New), + }) + } + return FileSetFor(declPGF.Tok), // edits use declPGF.Tok + &analysis.SuggestedFix{TextEdits: edits}, + nil +} |