mirror of
https://github.com/golang/go
synced 2024-11-18 23:54:41 -07:00
f80fb1dfa1
The main goal is to push the package variant logic from internal/lsp into internal/lsp/source so all users of internal/lsp/source benefit. "references" and "rename" now have top-level source.References() and source.Rename() entry points (as opposed to hanging off source.Identifier()). I expanded objectsAtProtocolPos() to know about implicit objects (type switch and import spec), and to handle *ast.ImportSpec generically. This gets rid of special case handling of *types.PkgName in various places. The biggest practical benefit, though, is that "references" no longer needs to compute the objectpath for every types.Object comparison it does, instead using direct types.Object equality. This speeds up "references" and "rename" a lot. Two other notable improvements that fell out of not using source.Identifier()'s logic: - Finding references on an embedded field now shows references to the field, not the type being embedded. - Finding references on an imported object now works correctly (previously it searched the importing package's dependents rather than the imported package's dependents). Finally, I refactored findIdentifier() to use pathEnclosingObjNode() instead of astutil.PathEnclosingInterval. Now we only need a single call to get the path because pathEnclosingObjNode() has the "try pos || try pos-1" logic built in. Change-Id: I667be9bed6ad83912404b90257c5c1485b3a7025 Reviewed-on: https://go-review.googlesource.com/c/tools/+/211999 Run-TryBot: Muir Manders <muir@mnd.rs> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
309 lines
7.9 KiB
Go
309 lines
7.9 KiB
Go
// 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 source
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/token"
|
|
"go/types"
|
|
"regexp"
|
|
|
|
"golang.org/x/tools/go/types/typeutil"
|
|
"golang.org/x/tools/internal/lsp/diff"
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
"golang.org/x/tools/internal/span"
|
|
"golang.org/x/tools/internal/telemetry/trace"
|
|
"golang.org/x/tools/refactor/satisfy"
|
|
errors "golang.org/x/xerrors"
|
|
)
|
|
|
|
type renamer struct {
|
|
ctx context.Context
|
|
fset *token.FileSet
|
|
refs []*ReferenceInfo
|
|
objsToUpdate map[types.Object]bool
|
|
hadConflicts bool
|
|
errors string
|
|
from, to string
|
|
satisfyConstraints map[satisfy.Constraint]bool
|
|
packages map[*types.Package]Package // may include additional packages that are a rdep of pkg
|
|
msets typeutil.MethodSetCache
|
|
changeMethods bool
|
|
}
|
|
|
|
type PrepareItem struct {
|
|
Range protocol.Range
|
|
Text string
|
|
}
|
|
|
|
func PrepareRename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position) (*PrepareItem, error) {
|
|
ctx, done := trace.StartSpan(ctx, "source.PrepareRename")
|
|
defer done()
|
|
|
|
qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Do not rename builtin identifiers.
|
|
if qos[0].obj.Parent() == types.Universe {
|
|
return nil, errors.Errorf("cannot rename builtin %q", qos[0].obj.Name())
|
|
}
|
|
|
|
mr, err := posToMappedRange(s.View(), qos[0].sourcePkg, qos[0].node.Pos(), qos[0].node.End())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rng, err := mr.Range()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, isImport := qos[0].node.(*ast.ImportSpec); isImport {
|
|
// We're not really renaming the import path.
|
|
rng.End = rng.Start
|
|
}
|
|
|
|
return &PrepareItem{
|
|
Range: rng,
|
|
Text: qos[0].obj.Name(),
|
|
}, nil
|
|
}
|
|
|
|
// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
|
|
func Rename(ctx context.Context, s Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, error) {
|
|
ctx, done := trace.StartSpan(ctx, "source.Rename")
|
|
defer done()
|
|
|
|
qos, err := qualifiedObjsAtProtocolPos(ctx, s, f, pp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
obj := qos[0].obj
|
|
pkg := qos[0].pkg
|
|
sourcePkg := qos[0].sourcePkg
|
|
|
|
if obj.Name() == newName {
|
|
return nil, errors.Errorf("old and new names are the same: %s", newName)
|
|
}
|
|
if !isValidIdentifier(newName) {
|
|
return nil, errors.Errorf("invalid identifier to rename: %q", newName)
|
|
}
|
|
// Do not rename builtin identifiers.
|
|
if obj.Parent() == types.Universe {
|
|
return nil, errors.Errorf("cannot rename builtin %q", obj.Name())
|
|
}
|
|
if pkg == nil || pkg.IsIllTyped() {
|
|
return nil, errors.Errorf("package for %s is ill typed", f.Identity().URI)
|
|
}
|
|
// Do not rename identifiers declared in another package.
|
|
if pkg != sourcePkg {
|
|
return nil, errors.Errorf("rename failed because %q is declared in a different package (%s)", obj.Name(), pkg.PkgPath())
|
|
}
|
|
|
|
refs, err := References(ctx, s, f, pp, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := renamer{
|
|
ctx: ctx,
|
|
fset: s.View().Session().Cache().FileSet(),
|
|
refs: refs,
|
|
objsToUpdate: make(map[types.Object]bool),
|
|
from: obj.Name(),
|
|
to: newName,
|
|
packages: make(map[*types.Package]Package),
|
|
}
|
|
for _, from := range refs {
|
|
r.packages[from.pkg.GetTypes()] = from.pkg
|
|
}
|
|
|
|
// Check that the renaming of the identifier is ok.
|
|
for _, ref := range refs {
|
|
r.check(ref.obj)
|
|
if r.hadConflicts { // one error is enough.
|
|
break
|
|
}
|
|
}
|
|
if r.hadConflicts {
|
|
return nil, errors.Errorf(r.errors)
|
|
}
|
|
|
|
changes, err := r.update()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(map[span.URI][]protocol.TextEdit)
|
|
for uri, edits := range changes {
|
|
// These edits should really be associated with FileHandles for maximal correctness.
|
|
// For now, this is good enough.
|
|
fh, err := s.GetFile(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, _, err := fh.Read(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
converter := span.NewContentConverter(uri.Filename(), data)
|
|
m := &protocol.ColumnMapper{
|
|
URI: uri,
|
|
Converter: converter,
|
|
Content: data,
|
|
}
|
|
// Sort the edits first.
|
|
diff.SortTextEdits(edits)
|
|
protocolEdits, err := ToProtocolEdits(m, edits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[uri] = protocolEdits
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// Rename all references to the identifier.
|
|
func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
|
|
result := make(map[span.URI][]diff.TextEdit)
|
|
seen := make(map[span.Span]bool)
|
|
|
|
docRegexp, err := regexp.Compile(`\b` + r.from + `\b`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ref := range r.refs {
|
|
refSpan, err := ref.spanRange.Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if seen[refSpan] {
|
|
continue
|
|
}
|
|
seen[refSpan] = true
|
|
|
|
// Renaming a types.PkgName may result in the addition or removal of an identifier,
|
|
// so we deal with this separately.
|
|
if pkgName, ok := ref.obj.(*types.PkgName); ok && ref.isDeclaration {
|
|
edit, err := r.updatePkgName(pkgName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[refSpan.URI()] = append(result[refSpan.URI()], *edit)
|
|
continue
|
|
}
|
|
|
|
// Replace the identifier with r.to.
|
|
edit := diff.TextEdit{
|
|
Span: refSpan,
|
|
NewText: r.to,
|
|
}
|
|
|
|
result[refSpan.URI()] = append(result[refSpan.URI()], edit)
|
|
|
|
if !ref.isDeclaration || ref.ident == nil { // uses do not have doc comments to update.
|
|
continue
|
|
}
|
|
|
|
doc := r.docComment(ref.pkg, ref.ident)
|
|
if doc == nil {
|
|
continue
|
|
}
|
|
|
|
// Perform the rename in doc comments declared in the original package.
|
|
for _, comment := range doc.List {
|
|
for _, locs := range docRegexp.FindAllStringIndex(comment.Text, -1) {
|
|
rng := span.NewRange(r.fset, comment.Pos()+token.Pos(locs[0]), comment.Pos()+token.Pos(locs[1]))
|
|
spn, err := rng.Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
|
|
Span: spn,
|
|
NewText: r.to,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// docComment returns the doc for an identifier.
|
|
func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
|
|
_, nodes, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, id.Pos(), id.End())
|
|
for _, node := range nodes {
|
|
switch decl := node.(type) {
|
|
case *ast.FuncDecl:
|
|
return decl.Doc
|
|
case *ast.Field:
|
|
return decl.Doc
|
|
case *ast.GenDecl:
|
|
return decl.Doc
|
|
// For {Type,Value}Spec, if the doc on the spec is absent,
|
|
// search for the enclosing GenDecl
|
|
case *ast.TypeSpec:
|
|
if decl.Doc != nil {
|
|
return decl.Doc
|
|
}
|
|
case *ast.ValueSpec:
|
|
if decl.Doc != nil {
|
|
return decl.Doc
|
|
}
|
|
case *ast.Ident:
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// updatePkgName returns the updates to rename a pkgName in the import spec
|
|
func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
|
|
// Modify ImportSpec syntax to add or remove the Name as needed.
|
|
pkg := r.packages[pkgName.Pkg()]
|
|
_, path, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, pkgName.Pos(), pkgName.Pos())
|
|
if len(path) < 2 {
|
|
return nil, errors.Errorf("no path enclosing interval for %s", pkgName.Name())
|
|
}
|
|
spec, ok := path[1].(*ast.ImportSpec)
|
|
if !ok {
|
|
return nil, errors.Errorf("failed to update PkgName for %s", pkgName.Name())
|
|
}
|
|
|
|
var astIdent *ast.Ident // will be nil if ident is removed
|
|
if pkgName.Imported().Name() != r.to {
|
|
// ImportSpec.Name needed
|
|
astIdent = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
|
|
}
|
|
|
|
// Make a copy of the ident that just has the name and path.
|
|
updated := &ast.ImportSpec{
|
|
Name: astIdent,
|
|
Path: spec.Path,
|
|
EndPos: spec.EndPos,
|
|
}
|
|
|
|
rng := span.NewRange(r.fset, spec.Pos(), spec.End())
|
|
spn, err := rng.Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
format.Node(&buf, r.fset, updated)
|
|
newText := buf.String()
|
|
|
|
return &diff.TextEdit{
|
|
Span: spn,
|
|
NewText: newText,
|
|
}, nil
|
|
}
|