blob: d2a52bc24775166de2b4e35cff8bc8a95bd34a00 [file] [log] [blame]
// 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
// TODO(adonovan):
//
// - method of generic concrete type -> arbitrary instances of same
//
// - make satisfy work across packages.
//
// - tests, tests, tests:
// - play with renamings in the k8s tree.
// - generics
// - error cases (e.g. conflicts)
// - renaming a symbol declared in the module cache
// (currently proceeds with half of the renaming!)
// - make sure all tests have both a local and a cross-package analogue.
// - look at coverage
// - special cases: embedded fields, interfaces, test variants,
// function-local things with uppercase names;
// packages with type errors (currently 'satisfy' rejects them),
// pakage with missing imports;
//
// - measure performance in k8s.
//
// - The original gorename tool assumed well-typedness, but the gopls feature
// does no such check (which actually makes it much more useful).
// Audit to ensure it is safe on ill-typed code.
//
// - Generics support was no doubt buggy before but incrementalization
// may have exacerbated it. If the problem were just about objects,
// defs and uses it would be fairly simple, but type assignability
// comes into play in the 'satisfy' check for method renamings.
// De-instantiating Vector[int] to Vector[T] changes its type.
// We need to come up with a theory for the satisfy check that
// works with generics, and across packages. We currently have no
// simple way to pass types between packages (think: objectpath for
// types), though presumably exportdata could be pressed into service.
//
// - FileID-based de-duplication of edits to different URIs for the same file.
import (
"context"
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/objectpath"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/safetoken"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/refactor/satisfy"
)
// A renamer holds state of a single call to renameObj, which renames
// an object (or several coupled objects) within a single type-checked
// syntax package.
type renamer struct {
pkg Package // the syntax package in which the renaming is applied
objsToUpdate map[types.Object]bool // records progress of calls to check
hadConflicts bool
conflicts []string
from, to string
satisfyConstraints map[satisfy.Constraint]bool
msets typeutil.MethodSetCache
changeMethods bool
}
// A PrepareItem holds the result of a "prepare rename" operation:
// the source range and value of a selected identifier.
type PrepareItem struct {
Range protocol.Range
Text string
}
// PrepareRename searches for a valid renaming at position pp.
//
// The returned usererr is intended to be displayed to the user to explain why
// the prepare fails. Probably we could eliminate the redundancy in returning
// two errors, but for now this is done defensively.
func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) {
ctx, done := event.Start(ctx, "source.PrepareRename")
defer done()
// Is the cursor within the package name declaration?
if pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp); err != nil {
return nil, err, err
} else if inPackageName {
item, err := prepareRenamePackageName(ctx, snapshot, pgf)
return item, err, err
}
// Ordinary (non-package) renaming.
//
// Type-check the current package, locate the reference at the position,
// validate the object, and report its name and range.
//
// TODO(adonovan): in all cases below, we return usererr=nil,
// which means we return (nil, nil) at the protocol
// layer. This seems like a bug, or at best an exploitation of
// knowledge of VSCode-specific behavior. Can we avoid that?
pkg, pgf, err := PackageForFile(ctx, snapshot, f.URI(), NarrowestPackage)
if err != nil {
return nil, nil, err
}
pos, err := pgf.PositionPos(pp)
if err != nil {
return nil, nil, err
}
targets, node, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos)
if err != nil {
return nil, nil, err
}
var obj types.Object
for obj = range targets {
break // pick one arbitrarily
}
if err := checkRenamable(obj); err != nil {
return nil, nil, err
}
rng, err := pgf.NodeRange(node)
if err != nil {
return nil, nil, err
}
if _, isImport := node.(*ast.ImportSpec); isImport {
// We're not really renaming the import path.
rng.End = rng.Start
}
return &PrepareItem{
Range: rng,
Text: obj.Name(),
}, nil, nil
}
func prepareRenamePackageName(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile) (*PrepareItem, error) {
// Does the client support file renaming?
fileRenameSupported := false
for _, op := range snapshot.View().Options().SupportedResourceOperations {
if op == protocol.Rename {
fileRenameSupported = true
break
}
}
if !fileRenameSupported {
return nil, errors.New("can't rename package: LSP client does not support file renaming")
}
// Check validity of the metadata for the file's containing package.
fileMeta, err := snapshot.MetadataForFile(ctx, pgf.URI)
if err != nil {
return nil, err
}
if len(fileMeta) == 0 {
return nil, fmt.Errorf("no packages found for file %q", pgf.URI)
}
meta := fileMeta[0]
if meta.Name == "main" {
return nil, fmt.Errorf("can't rename package \"main\"")
}
if strings.HasSuffix(string(meta.Name), "_test") {
return nil, fmt.Errorf("can't rename x_test packages")
}
if meta.Module == nil {
return nil, fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath)
}
if meta.Module.Path == string(meta.PkgPath) {
return nil, fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path)
}
// Return the location of the package declaration.
rng, err := pgf.NodeRange(pgf.File.Name)
if err != nil {
return nil, err
}
return &PrepareItem{
Range: rng,
Text: string(meta.Name),
}, nil
}
func checkRenamable(obj types.Object) error {
switch obj := obj.(type) {
case *types.Var:
if obj.Embedded() {
return fmt.Errorf("can't rename embedded fields: rename the type directly or name the field")
}
case *types.Builtin, *types.Nil:
return fmt.Errorf("%s is built in and cannot be renamed", obj.Name())
}
if obj.Pkg() == nil || obj.Pkg().Path() == "unsafe" {
// e.g. error.Error, unsafe.Pointer
return fmt.Errorf("%s is built in and cannot be renamed", obj.Name())
}
if obj.Name() == "_" {
return errors.New("can't rename \"_\"")
}
return nil
}
// Rename returns a map of TextEdits for each file modified when renaming a
// given identifier within a package and a boolean value of true for renaming
// package and false otherwise.
func Rename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, bool, error) {
ctx, done := event.Start(ctx, "source.Rename")
defer done()
if !isValidIdentifier(newName) {
return nil, false, fmt.Errorf("invalid identifier to rename: %q", newName)
}
// Cursor within package name declaration?
_, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp)
if err != nil {
return nil, false, err
}
var editMap map[span.URI][]diff.Edit
if inPackageName {
editMap, err = renamePackageName(ctx, snapshot, f, PackageName(newName))
} else {
editMap, err = renameOrdinary(ctx, snapshot, f, pp, newName)
}
if err != nil {
return nil, false, err
}
// Convert edits to protocol form.
result := make(map[span.URI][]protocol.TextEdit)
for uri, edits := range editMap {
// Sort and de-duplicate edits.
//
// Overlapping edits may arise in local renamings (due
// to type switch implicits) and globals ones (due to
// processing multiple package variants).
//
// We assume renaming produces diffs that are all
// replacements (no adjacent insertions that might
// become reordered) and that are either identical or
// non-overlapping.
diff.SortEdits(edits)
filtered := edits[:0]
for i, edit := range edits {
if i == 0 || edit != filtered[len(filtered)-1] {
filtered = append(filtered, edit)
}
}
edits = filtered
// TODO(adonovan): the logic above handles repeat edits to the
// same file URI (e.g. as a member of package p and p_test) but
// is not sufficient to handle file-system level aliasing arising
// from symbolic or hard links. For that, we should use a
// robustio-FileID-keyed map.
// See https://go.dev/cl/457615 for example.
// This really occurs in practice, e.g. kubernetes has
// vendor/k8s.io/kubectl -> ../../staging/src/k8s.io/kubectl.
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return nil, false, err
}
data, err := fh.Content()
if err != nil {
return nil, false, err
}
m := protocol.NewMapper(uri, data)
protocolEdits, err := ToProtocolEdits(m, edits)
if err != nil {
return nil, false, err
}
result[uri] = protocolEdits
}
return result, inPackageName, nil
}
// renameOrdinary renames an ordinary (non-package) name throughout the workspace.
func renameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]diff.Edit, error) {
// Type-check the referring package and locate the object(s).
// We choose the widest variant as, for non-exported
// identifiers, it is the only package we need.
pkg, pgf, err := PackageForFile(ctx, snapshot, f.URI(), WidestPackage)
if err != nil {
return nil, err
}
pos, err := pgf.PositionPos(pp)
if err != nil {
return nil, err
}
targets, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos)
if err != nil {
return nil, err
}
// Pick a representative object arbitrarily.
// (All share the same name, pos, and kind.)
var obj types.Object
for obj = range targets {
break
}
if obj.Name() == newName {
return nil, fmt.Errorf("old and new names are the same: %s", newName)
}
if err := checkRenamable(obj); err != nil {
return nil, err
}
// Find objectpath, if object is exported ("" otherwise).
var declObjPath objectpath.Path
if obj.Exported() {
// objectpath.For requires the origin of a generic
// function or type, not an instantiation (a bug?).
// Unfortunately we can't call {Func,TypeName}.Origin
// as these are not available in go/types@go1.18.
// So we take a scenic route.
switch obj.(type) { // avoid "obj :=" since cases reassign the var
case *types.TypeName:
if named, ok := obj.Type().(*types.Named); ok {
obj = named.Obj()
}
case *types.Func:
obj = funcOrigin(obj.(*types.Func))
case *types.Var:
// TODO(adonovan): do vars need the origin treatment too? (issue #58462)
}
if path, err := objectpath.For(obj); err == nil {
declObjPath = path
}
}
// Nonexported? Search locally.
if declObjPath == "" {
var objects []types.Object
for obj := range targets {
objects = append(objects, obj)
}
editMap, _, err := renameObjects(ctx, snapshot, newName, pkg, objects...)
return editMap, err
}
// Exported: search globally.
//
// For exported package-level var/const/func/type objects, the
// search scope is just the direct importers.
//
// For exported fields and methods, the scope is the
// transitive rdeps. (The exportedness of the field's struct
// or method's receiver is irrelevant.)
transitive := false
switch obj.(type) {
case *types.TypeName:
// Renaming an exported package-level type
// requires us to inspect all transitive rdeps
// in the event that the type is embedded.
//
// TODO(adonovan): opt: this is conservative
// but inefficient. Instead, expand the scope
// of the search only if we actually encounter
// an embedding of the type, and only then to
// the rdeps of the embedding package.
if obj.Parent() == obj.Pkg().Scope() {
transitive = true
}
case *types.Var:
if obj.(*types.Var).IsField() {
transitive = true // field
}
// TODO(adonovan): opt: process only packages that
// contain a reference (xrefs) to the target field.
case *types.Func:
if obj.Type().(*types.Signature).Recv() != nil {
transitive = true // method
}
// It's tempting to optimize by skipping
// packages that don't contain a reference to
// the method in the xrefs index, but we still
// need to apply the satisfy check to those
// packages to find assignment statements that
// might expands the scope of the renaming.
}
// Type-check all the packages to inspect.
declURI := span.URIFromPath(pkg.FileSet().File(obj.Pos()).Name())
pkgs, err := typeCheckReverseDependencies(ctx, snapshot, declURI, transitive)
if err != nil {
return nil, err
}
// Apply the renaming to the (initial) object.
declPkgPath := PackagePath(obj.Pkg().Path())
return renameExported(ctx, snapshot, pkgs, declPkgPath, declObjPath, newName)
}
// funcOrigin is a go1.18-portable implementation of (*types.Func).Origin.
func funcOrigin(fn *types.Func) *types.Func {
// Method?
if fn.Type().(*types.Signature).Recv() != nil {
return typeparams.OriginMethod(fn)
}
// Package-level function?
// (Assume the origin has the same position.)
gen := fn.Pkg().Scope().Lookup(fn.Name())
if gen != nil && gen.Pos() == fn.Pos() {
return gen.(*types.Func)
}
return fn
}
// typeCheckReverseDependencies returns the type-checked packages for
// the reverse dependencies of all packages variants containing
// file declURI. The packages are in some topological order.
//
// It includes all variants (even intermediate test variants) for the
// purposes of computing reverse dependencies, but discards ITVs for
// the actual renaming work.
//
// (This neglects obscure edge cases where a _test.go file changes the
// selectors used only in an ITV, but life is short. Also sin must be
// punished.)
func typeCheckReverseDependencies(ctx context.Context, snapshot Snapshot, declURI span.URI, transitive bool) ([]Package, error) {
variants, err := snapshot.MetadataForFile(ctx, declURI)
if err != nil {
return nil, err
}
allRdeps := make(map[PackageID]*Metadata)
for _, variant := range variants {
rdeps, err := snapshot.ReverseDependencies(ctx, variant.ID, transitive)
if err != nil {
return nil, err
}
allRdeps[variant.ID] = variant // include self
for id, meta := range rdeps {
allRdeps[id] = meta
}
}
var ids []PackageID
for id, meta := range allRdeps {
if meta.IsIntermediateTestVariant() {
continue
}
ids = append(ids, id)
}
// Sort the packages into some topological order of the
// (unfiltered) metadata graph.
SortPostOrder(snapshot, ids)
// Dependencies must be visited first since they can expand
// the search set. Ideally we would process the (filtered) set
// of packages in the parallel postorder of the snapshot's
// (unfiltered) metadata graph, but this is quite tricky
// without a good graph abstraction.
//
// For now, we visit packages sequentially in order of
// ascending height, like an inverted breadth-first search.
//
// Type checking is by far the dominant cost, so
// overlapping it with renaming may not be worthwhile.
return snapshot.TypeCheck(ctx, ids...)
}
// SortPostOrder sorts the IDs so that if x depends on y, then y appears before x.
func SortPostOrder(meta MetadataSource, ids []PackageID) {
postorder := make(map[PackageID]int)
order := 0
var visit func(PackageID)
visit = func(id PackageID) {
if _, ok := postorder[id]; !ok {
postorder[id] = -1 // break recursion
if m := meta.Metadata(id); m != nil {
for _, depID := range m.DepsByPkgPath {
visit(depID)
}
}
order++
postorder[id] = order
}
}
for _, id := range ids {
visit(id)
}
sort.Slice(ids, func(i, j int) bool {
return postorder[ids[i]] < postorder[ids[j]]
})
}
// renameExported renames the object denoted by (pkgPath, objPath)
// within the specified packages, along with any other objects that
// must be renamed as a consequence. The slice of packages must be
// topologically ordered.
func renameExported(ctx context.Context, snapshot Snapshot, pkgs []Package, declPkgPath PackagePath, declObjPath objectpath.Path, newName string) (map[span.URI][]diff.Edit, error) {
// A target is a name for an object that is stable across types.Packages.
type target struct {
pkg PackagePath
obj objectpath.Path
}
// Populate the initial set of target objects.
// This set may grow as we discover the consequences of each renaming.
//
// TODO(adonovan): strictly, each cone of reverse dependencies
// of a single variant should have its own target map that
// monotonically expands as we go up the import graph, because
// declarations in test files can alter the set of
// package-level names and change the meaning of field and
// method selectors. So if we parallelize the graph
// visitation (see above), we should also compute the targets
// as a union of dependencies.
//
// Or we could decide that the logic below is fast enough not
// to need parallelism. In small measurements so far the
// type-checking step is about 95% and the renaming only 5%.
targets := map[target]bool{{declPkgPath, declObjPath}: true}
// Apply the renaming operation to each package.
allEdits := make(map[span.URI][]diff.Edit)
for _, pkg := range pkgs {
// Resolved target objects within package pkg.
var objects []types.Object
for t := range targets {
p := pkg.DependencyTypes(t.pkg)
if p == nil {
continue // indirect dependency of no consequence
}
obj, err := objectpath.Object(p, t.obj)
if err != nil {
// Though this can happen with regular export data
// due to trimming of inconsequential objects,
// it can't happen if we load dependencies from full
// syntax (as today) or shallow export data (soon),
// as both are complete.
bug.Reportf("objectpath.Object(%v, %v) failed: %v", p, t.obj, err)
continue
}
objects = append(objects, obj)
}
if len(objects) == 0 {
continue // no targets of consequence to this package
}
// Apply the renaming.
editMap, moreObjects, err := renameObjects(ctx, snapshot, newName, pkg, objects...)
if err != nil {
return nil, err
}
// It is safe to concatenate the edits as they are non-overlapping
// (or identical, in which case they will be de-duped by Rename).
for uri, edits := range editMap {
allEdits[uri] = append(allEdits[uri], edits...)
}
// Expand the search set?
for obj := range moreObjects {
objpath, err := objectpath.For(obj)
if err != nil {
continue // not exported
}
target := target{PackagePath(obj.Pkg().Path()), objpath}
targets[target] = true
// TODO(adonovan): methods requires dynamic
// programming of the product targets x
// packages as any package might add a new
// target (from a foward dep) as a
// consequence, and any target might imply a
// new set of rdeps. See golang/go#58461.
}
}
return allEdits, nil
}
// renamePackageName renames package declarations, imports, and go.mod files.
func renamePackageName(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[span.URI][]diff.Edit, error) {
// Rename the package decl and all imports.
renamingEdits, err := renamePackage(ctx, s, f, newName)
if err != nil {
return nil, err
}
// Update the last component of the file's enclosing directory.
oldBase := filepath.Dir(f.URI().Filename())
newPkgDir := filepath.Join(filepath.Dir(oldBase), string(newName))
// Update any affected replace directives in go.mod files.
// TODO(adonovan): extract into its own function.
//
// TODO: should this operate on all go.mod files, irrespective of whether they are included in the workspace?
// Get all active mod files in the workspace
modFiles := s.ModFiles()
for _, m := range modFiles {
fh, err := s.ReadFile(ctx, m)
if err != nil {
return nil, err
}
pm, err := s.ParseMod(ctx, fh)
if err != nil {
return nil, err
}
modFileDir := filepath.Dir(pm.URI.Filename())
affectedReplaces := []*modfile.Replace{}
// Check if any replace directives need to be fixed
for _, r := range pm.File.Replace {
if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") {
continue
}
replacedPath := r.New.Path
if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") {
replacedPath = filepath.Join(modFileDir, r.New.Path)
}
// TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement?
if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") {
continue // not affected by the package renaming
}
affectedReplaces = append(affectedReplaces, r)
}
if len(affectedReplaces) == 0 {
continue
}
copied, err := modfile.Parse("", pm.Mapper.Content, nil)
if err != nil {
return nil, err
}
for _, r := range affectedReplaces {
replacedPath := r.New.Path
if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") {
replacedPath = filepath.Join(modFileDir, r.New.Path)
}
suffix := strings.TrimPrefix(replacedPath, string(oldBase))
newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix)
if err != nil {
return nil, err
}
newReplacedPath = filepath.ToSlash(newReplacedPath)
if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") {
newReplacedPath = "./" + newReplacedPath
}
if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil {
return nil, err
}
}
copied.Cleanup()
newContent, err := copied.Format()
if err != nil {
return nil, err
}
// Calculate the edits to be made due to the change.
edits := s.View().Options().ComputeEdits(string(pm.Mapper.Content), string(newContent))
renamingEdits[pm.URI] = append(renamingEdits[pm.URI], edits...)
}
return renamingEdits, nil
}
// renamePackage computes all workspace edits required to rename the package
// described by the given metadata, to newName, by renaming its package
// directory.
//
// It updates package clauses and import paths for the renamed package as well
// as any other packages affected by the directory renaming among packages
// described by allMetadata.
func renamePackage(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[span.URI][]diff.Edit, error) {
if strings.HasSuffix(string(newName), "_test") {
return nil, fmt.Errorf("cannot rename to _test package")
}
// We need metadata for the relevant package and module paths.
// These should be the same for all packages containing the file.
metas, err := s.MetadataForFile(ctx, f.URI())
if err != nil {
return nil, err
}
if len(metas) == 0 {
return nil, fmt.Errorf("no packages found for file %q", f.URI())
}
meta := metas[0] // narrowest
oldPkgPath := meta.PkgPath
if meta.Module == nil {
return nil, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath)
}
modulePath := PackagePath(meta.Module.Path)
if modulePath == oldPkgPath {
return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath)
}
newPathPrefix := path.Join(path.Dir(string(oldPkgPath)), string(newName))
// We must inspect all packages, not just direct importers,
// because we also rename subpackages, which may be unrelated.
// (If the renamed package imports a subpackage it may require
// edits to both its package and import decls.)
allMetadata, err := s.AllMetadata(ctx)
if err != nil {
return nil, err
}
// Rename package and import declarations in all relevant packages.
edits := make(map[span.URI][]diff.Edit)
for _, m := range allMetadata {
// Special case: x_test packages for the renamed package will not have the
// package path as as a dir prefix, but still need their package clauses
// renamed.
if m.PkgPath == oldPkgPath+"_test" {
if err := renamePackageClause(ctx, m, s, newName+"_test", edits); err != nil {
return nil, err
}
continue
}
// Subtle: check this condition before checking for valid module info
// below, because we should not fail this operation if unrelated packages
// lack module info.
if !strings.HasPrefix(string(m.PkgPath)+"/", string(oldPkgPath)+"/") {
continue // not affected by the package renaming
}
if m.Module == nil {
// This check will always fail under Bazel.
return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PkgPath)
}
if modulePath != PackagePath(m.Module.Path) {
continue // don't edit imports if nested package and renaming package have different module paths
}
// Renaming a package consists of changing its import path and package name.
suffix := strings.TrimPrefix(string(m.PkgPath), string(oldPkgPath))
newPath := newPathPrefix + suffix
pkgName := m.Name
if m.PkgPath == oldPkgPath {
pkgName = PackageName(newName)
if err := renamePackageClause(ctx, m, s, newName, edits); err != nil {
return nil, err
}
}
imp := ImportPath(newPath) // TODO(adonovan): what if newPath has vendor/ prefix?
if err := renameImports(ctx, s, m, imp, pkgName, edits); err != nil {
return nil, err
}
}
return edits, nil
}
// renamePackageClause computes edits renaming the package clause of files in
// the package described by the given metadata, to newName.
//
// Edits are written into the edits map.
func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, newName PackageName, edits map[span.URI][]diff.Edit) error {
// Rename internal references to the package in the renaming package.
for _, uri := range m.CompiledGoFiles {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return err
}
f, err := snapshot.ParseGo(ctx, fh, ParseHeader)
if err != nil {
return err
}
if f.File.Name == nil {
continue // no package declaration
}
edit, err := posEdit(f.Tok, f.File.Name.Pos(), f.File.Name.End(), string(newName))
if err != nil {
return err
}
edits[f.URI] = append(edits[f.URI], edit)
}
return nil
}
// renameImports computes the set of edits to imports resulting from renaming
// the package described by the given metadata, to a package with import path
// newPath and name newName.
//
// Edits are written into the edits map.
func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath ImportPath, newName PackageName, allEdits map[span.URI][]diff.Edit) error {
rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, false) // find direct importers
if err != nil {
return err
}
// Pass 1: rename import paths in import declarations.
needsTypeCheck := make(map[PackageID][]span.URI)
for _, rdep := range rdeps {
if rdep.IsIntermediateTestVariant() {
continue // for renaming, these variants are redundant
}
for _, uri := range rdep.CompiledGoFiles {
fh, err := snapshot.ReadFile(ctx, uri)
if err != nil {
return err
}
f, err := snapshot.ParseGo(ctx, fh, ParseHeader)
if err != nil {
return err
}
if f.File.Name == nil {
continue // no package declaration
}
for _, imp := range f.File.Imports {
if rdep.DepsByImpPath[UnquoteImportPath(imp)] != m.ID {
continue // not the import we're looking for
}
// If the import does not explicitly specify
// a local name, then we need to invoke the
// type checker to locate references to update.
//
// TODO(adonovan): is this actually true?
// Renaming an import with a local name can still
// cause conflicts: shadowing of built-ins, or of
// package-level decls in the same or another file.
if imp.Name == nil {
needsTypeCheck[rdep.ID] = append(needsTypeCheck[rdep.ID], uri)
}
// Create text edit for the import path (string literal).
edit, err := posEdit(f.Tok, imp.Path.Pos(), imp.Path.End(), strconv.Quote(string(newPath)))
if err != nil {
return err
}
allEdits[uri] = append(allEdits[uri], edit)
}
}
}
// If the imported package's name hasn't changed,
// we don't need to rename references within each file.
if newName == m.Name {
return nil
}
// Pass 2: rename local name (types.PkgName) of imported
// package throughout one or more files of the package.
ids := make([]PackageID, 0, len(needsTypeCheck))
for id := range needsTypeCheck {
ids = append(ids, id)
}
pkgs, err := snapshot.TypeCheck(ctx, ids...)
if err != nil {
return err
}
for i, id := range ids {
pkg := pkgs[i]
for _, uri := range needsTypeCheck[id] {
f, err := pkg.File(uri)
if err != nil {
return err
}
for _, imp := range f.File.Imports {
if imp.Name != nil {
continue // has explicit local name
}
if rdeps[id].DepsByImpPath[UnquoteImportPath(imp)] != m.ID {
continue // not the import we're looking for
}
pkgname := pkg.GetTypesInfo().Implicits[imp].(*types.PkgName)
pkgScope := pkg.GetTypes().Scope()
fileScope := pkg.GetTypesInfo().Scopes[f.File]
localName := string(newName)
try := 0
// Keep trying with fresh names until one succeeds.
//
// TODO(adonovan): fix: this loop is not sufficient to choose a name
// that is guaranteed to be conflict-free; renameObj may still fail.
// So the retry loop should be around renameObj, and we shouldn't
// bother with scopes here.
for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil {
try++
localName = fmt.Sprintf("%s%d", newName, try)
}
// renameObj detects various conflicts, including:
// - new name conflicts with a package-level decl in this file;
// - new name hides a package-level decl in another file that
// is actually referenced in this file;
// - new name hides a built-in that is actually referenced
// in this file;
// - a reference in this file to the old package name would
// become shadowed by an intervening declaration that
// uses the new name.
// It returns the edits if no conflict was detected.
editMap, _, err := renameObjects(ctx, snapshot, localName, pkg, pkgname)
if err != nil {
return err
}
// If the chosen local package name matches the package's
// new name, delete the change that would have inserted
// an explicit local name, which is always the lexically
// first change.
if localName == string(newName) {
edits, ok := editMap[uri]
if !ok {
return fmt.Errorf("internal error: no changes for %s", uri)
}
diff.SortEdits(edits)
editMap[uri] = edits[1:]
}
for uri, edits := range editMap {
allEdits[uri] = append(allEdits[uri], edits...)
}
}
}
}
return nil
}
// renameObjects computes the edits to the type-checked syntax package pkg
// required to rename a set of target objects to newName.
//
// It also returns the set of objects that were found (due to
// corresponding methods and embedded fields) to require renaming as a
// consequence of the requested renamings.
//
// It returns an error if the renaming would cause a conflict.
func renameObjects(ctx context.Context, snapshot Snapshot, newName string, pkg Package, targets ...types.Object) (map[span.URI][]diff.Edit, map[types.Object]bool, error) {
r := renamer{
pkg: pkg,
objsToUpdate: make(map[types.Object]bool),
from: targets[0].Name(),
to: newName,
}
// A renaming initiated at an interface method indicates the
// intention to rename abstract and concrete methods as needed
// to preserve assignability.
// TODO(adonovan): pull this into the caller.
for _, obj := range targets {
if obj, ok := obj.(*types.Func); ok {
recv := obj.Type().(*types.Signature).Recv()
if recv != nil && types.IsInterface(recv.Type().Underlying()) {
r.changeMethods = true
break
}
}
}
// Check that the renaming of the identifier is ok.
for _, obj := range targets {
r.check(obj)
if len(r.conflicts) > 0 {
// Stop at first error.
return nil, nil, fmt.Errorf("%s", strings.Join(r.conflicts, "\n"))
}
}
editMap, err := r.update()
if err != nil {
return nil, nil, err
}
// Remove initial targets so that only 'consequences' remain.
for _, obj := range targets {
delete(r.objsToUpdate, obj)
}
return editMap, r.objsToUpdate, nil
}
// Rename all references to the target objects.
func (r *renamer) update() (map[span.URI][]diff.Edit, error) {
result := make(map[span.URI][]diff.Edit)
// shouldUpdate reports whether obj is one of (or an
// instantiation of one of) the target objects.
shouldUpdate := func(obj types.Object) bool {
if r.objsToUpdate[obj] {
return true
}
if fn, ok := obj.(*types.Func); ok && r.objsToUpdate[funcOrigin(fn)] {
return true
}
return false
}
// Find all identifiers in the package that define or use a
// renamed object. We iterate over info as it is more efficent
// than calling ast.Inspect for each of r.pkg.CompiledGoFiles().
type item struct {
node ast.Node // Ident, ImportSpec (obj=PkgName), or CaseClause (obj=Var)
obj types.Object
isDef bool
}
var items []item
info := r.pkg.GetTypesInfo()
for id, obj := range info.Uses {
if shouldUpdate(obj) {
items = append(items, item{id, obj, false})
}
}
for id, obj := range info.Defs {
if shouldUpdate(obj) {
items = append(items, item{id, obj, true})
}
}
for node, obj := range info.Implicits {
if shouldUpdate(obj) {
switch node.(type) {
case *ast.ImportSpec, *ast.CaseClause:
items = append(items, item{node, obj, true})
}
}
}
sort.Slice(items, func(i, j int) bool {
return items[i].node.Pos() < items[j].node.Pos()
})
// Update each identifier.
for _, item := range items {
pgf, ok := enclosingFile(r.pkg, item.node.Pos())
if !ok {
bug.Reportf("edit does not belong to syntax of package %q", r.pkg)
continue
}
// Renaming a types.PkgName may result in the addition or removal of an identifier,
// so we deal with this separately.
if pkgName, ok := item.obj.(*types.PkgName); ok && item.isDef {
edit, err := r.updatePkgName(pgf, pkgName)
if err != nil {
return nil, err
}
result[pgf.URI] = append(result[pgf.URI], edit)
continue
}
// Workaround the unfortunate lack of a Var object
// for x in "switch x := expr.(type) {}" by adjusting
// the case clause to the switch ident.
// This may result in duplicate edits, but we de-dup later.
if _, ok := item.node.(*ast.CaseClause); ok {
path, _ := astutil.PathEnclosingInterval(pgf.File, item.obj.Pos(), item.obj.Pos())
item.node = path[0].(*ast.Ident)
}
// Replace the identifier with r.to.
edit, err := posEdit(pgf.Tok, item.node.Pos(), item.node.End(), r.to)
if err != nil {
return nil, err
}
result[pgf.URI] = append(result[pgf.URI], edit)
if !item.isDef { // uses do not have doc comments to update.
continue
}
doc := docComment(pgf, item.node.(*ast.Ident))
if doc == nil {
continue
}
// Perform the rename in doc comments declared in the original package.
// go/parser strips out \r\n returns from the comment text, so go
// line-by-line through the comment text to get the correct positions.
docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) // valid identifier => valid regexp
for _, comment := range doc.List {
if isDirective(comment.Text) {
continue
}
// TODO(adonovan): why are we looping over lines?
// Just run the loop body once over the entire multiline comment.
lines := strings.Split(comment.Text, "\n")
tokFile := pgf.Tok
commentLine := tokFile.Line(comment.Pos())
uri := span.URIFromPath(tokFile.Name())
for i, line := range lines {
lineStart := comment.Pos()
if i > 0 {
lineStart = tokFile.LineStart(commentLine + i)
}
for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) {
edit, err := posEdit(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]), r.to)
if err != nil {
return nil, err // can't happen
}
result[uri] = append(result[uri], edit)
}
}
}
}
return result, nil
}
// docComment returns the doc for an identifier within the specified file.
func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup {
nodes, _ := astutil.PathEnclosingInterval(pgf.File, 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:
case *ast.AssignStmt:
// *ast.AssignStmt doesn't have an associated comment group.
// So, we try to find a comment just before the identifier.
// Try to find a comment group only for short variable declarations (:=).
if decl.Tok != token.DEFINE {
return nil
}
identLine := pgf.Tok.Line(id.Pos())
for _, comment := range nodes[len(nodes)-1].(*ast.File).Comments {
if comment.Pos() > id.Pos() {
// Comment is after the identifier.
continue
}
lastCommentLine := pgf.Tok.Line(comment.End())
if lastCommentLine+1 == identLine {
return comment
}
}
default:
return nil
}
}
return nil
}
// updatePkgName returns the updates to rename a pkgName in the import spec by
// only modifying the package name portion of the import declaration.
func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff.Edit, error) {
// Modify ImportSpec syntax to add or remove the Name as needed.
path, _ := astutil.PathEnclosingInterval(pgf.File, pkgName.Pos(), pkgName.Pos())
if len(path) < 2 {
return diff.Edit{}, fmt.Errorf("no path enclosing interval for %s", pkgName.Name())
}
spec, ok := path[1].(*ast.ImportSpec)
if !ok {
return diff.Edit{}, fmt.Errorf("failed to update PkgName for %s", pkgName.Name())
}
newText := ""
if pkgName.Imported().Name() != r.to {
newText = r.to + " "
}
// Replace the portion (possibly empty) of the spec before the path:
// local "path" or "path"
// -> <- -><-
return posEdit(pgf.Tok, spec.Pos(), spec.Path.Pos(), newText)
}
// parsePackageNameDecl is a convenience function that parses and
// returns the package name declaration of file fh, and reports
// whether the position ppos lies within it.
//
// Note: also used by references.
func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) {
pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
if err != nil {
return nil, false, err
}
// Careful: because we used ParseHeader,
// pgf.Pos(ppos) may be beyond EOF => (0, err).
pos, _ := pgf.PositionPos(ppos)
return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil
}
// enclosingFile returns the CompiledGoFile of pkg that contains the specified position.
func enclosingFile(pkg Package, pos token.Pos) (*ParsedGoFile, bool) {
for _, pgf := range pkg.CompiledGoFiles() {
if pgf.File.Pos() <= pos && pos <= pgf.File.End() {
return pgf, true
}
}
return nil, false
}
// posEdit returns an edit to replace the (start, end) range of tf with 'new'.
func posEdit(tf *token.File, start, end token.Pos, new string) (diff.Edit, error) {
startOffset, endOffset, err := safetoken.Offsets(tf, start, end)
if err != nil {
return diff.Edit{}, err
}
return diff.Edit{Start: startOffset, End: endOffset, New: new}, nil
}