diff options
Diffstat (limited to 'gopls/internal/lsp/source/rename.go')
-rw-r--r-- | gopls/internal/lsp/source/rename.go | 1244 |
1 files changed, 1244 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go new file mode 100644 index 000000000..c67f15ce5 --- /dev/null +++ b/gopls/internal/lsp/source/rename.go @@ -0,0 +1,1244 @@ +// 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.GetFile(ctx, uri) + if err != nil { + return nil, false, err + } + data, err := fh.Read() + 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.GetFile(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.GetFile(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.GetFile(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 references2. +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 +} |