blob: c85080f8a0ce8a2ef1bde5c243f6804535dbf961 [file] [log] [blame]
// 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 golang
import (
"bytes"
"context"
"fmt"
"go/format"
"go/parser"
"go/token"
"go/types"
pathpkg "path"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/golang/stubmethods"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/tokeninternal"
)
// stubMissingInterfaceMethodsFixer 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 stubMissingInterfaceMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
si := stubmethods.GetIfaceStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end)
if si == nil {
return nil, nil, fmt.Errorf("nil interface request")
}
return insertDeclsAfter(ctx, snapshot, pkg.Metadata(), si.Fset, si.Concrete.Obj(), si.Emit)
}
// stubMissingCalledFunctionFixer returns a suggested fix to declare the missing
// method that the user may want to generate based on CallExpr
// at the cursor position.
func stubMissingCalledFunctionFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
si := stubmethods.GetCallStubInfo(pkg.FileSet(), pkg.TypesInfo(), pgf, start, end)
if si == nil {
return nil, nil, fmt.Errorf("invalid type request")
}
return insertDeclsAfter(ctx, snapshot, pkg.Metadata(), si.Fset, si.After, si.Emit)
}
// An emitter writes new top-level declarations into an existing
// file. References to symbols should be qualified using qual, which
// respects the local import environment.
type emitter = func(out *bytes.Buffer, qual types.Qualifier) error
// insertDeclsAfter locates the file that declares symbol sym,
// (which must be among the dependencies of mp),
// calls the emit function to generate new declarations,
// respecting the local import environment,
// and splices those declarations into the file after the declaration of sym,
// updating imports as needed.
//
// fset must provide the position of sym.
func insertDeclsAfter(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package, fset *token.FileSet, sym types.Object, emit emitter) (*token.FileSet, *analysis.SuggestedFix, error) {
// Parse the file declaring the sym.
//
// Beware: declPGF is not necessarily covered by pkg.FileSet() or si.Fset.
declPGF, _, err := parseFull(ctx, snapshot, fset, sym.Pos())
if err != nil {
return nil, nil, fmt.Errorf("failed to parse file %q declaring implementation symbol: %w", declPGF.URI, err)
}
if declPGF.Fixed() {
return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI)
}
// Find metadata for the symbol's declaring package
// as we'll need its import mapping.
declMeta := findFileInDeps(snapshot, mp, declPGF.URI)
if declMeta == nil {
return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, mp)
}
// Build import environment for the declaring file.
// (typesinternal.FileQualifier works only for complete
// import mappings, and requires types.)
importEnv := make(map[ImportPath]string) // value is local name
for _, imp := range declPGF.File.Imports {
importPath := metadata.UnquoteImportPath(imp)
var name string
if imp.Name != nil {
name = imp.Name.Name
if name == "_" {
continue
} else if name == "." {
name = "" // see types.Qualifier
}
} else {
// Use the correct name from the metadata of the imported
// package---not a guess based on the import path.
mp := snapshot.Metadata(declMeta.DepsByImpPath[importPath])
if mp == nil {
continue // can't happen?
}
name = string(mp.Name)
}
importEnv[importPath] = name // latest alias wins
}
// Create a package name qualifier that uses the
// locally appropriate imported package name.
// It records any needed new imports.
// TODO(adonovan): factor with golang.FormatVarType?
//
// 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.
//
// Ignore the current package import.
if pkg.Path() == sym.Pkg().Path() {
return ""
}
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 (sym.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 != pathpkg.Base(trimVersionSuffix(new.importPath)) {
new.name = name
}
newImports = append(newImports, new)
}
return name
}
// Compute insertion point for new declarations:
// 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)
}
symOffset, err := safetoken.Offset(fset.File(sym.Pos()), sym.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 > symOffset {
insertOffset = declEndOffset
break
}
}
// Splice the new declarations into the file content.
var buf bytes.Buffer
input := declPGF.Mapper.Content // unfixed content of file
buf.Write(input[:insertOffset])
buf.WriteByte('\n')
err = emit(&buf, qual)
if err != nil {
return nil, nil, err
}
buf.Write(input[insertOffset:])
// Re-parse the file.
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)
}
// Splice the new imports into the syntax tree.
for _, imp := range newImports {
astutil.AddNamedImport(fset, newF, imp.name, imp.importPath)
}
// Pretty-print.
var output bytes.Buffer
if err := format.Node(&output, fset, newF); err != nil {
return nil, nil, fmt.Errorf("format.Node: %w", err)
}
// Report the diff.
diffs := diff.Bytes(input, output.Bytes())
return tokeninternal.FileSetFor(declPGF.Tok), // edits use declPGF.Tok
&analysis.SuggestedFix{TextEdits: diffToTextEdits(declPGF.Tok, diffs)},
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))
for _, edit := range diffs {
edits = append(edits, analysis.TextEdit{
Pos: tok.Pos(edit.Start),
End: tok.Pos(edit.End),
NewText: []byte(edit.New),
})
}
return edits
}
// trimVersionSuffix removes a trailing "/v2" (etc) suffix from a module path.
//
// This is only a heuristic as to the package's declared name, and
// should only be used for stylistic decisions, such as whether it
// would be clearer to use an explicit local name in the import
// because the declared name differs from the result of this function.
// When the name matters for correctness, look up the imported
// package's Metadata.Name.
func trimVersionSuffix(path string) string {
dir, base := pathpkg.Split(path)
if len(base) > 1 && base[0] == 'v' && strings.Trim(base[1:], "0123456789") == "" {
return dir // sans "/v2"
}
return path
}