| // Copyright 2014 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 rename contains the obsolete implementation of the deleted |
| // golang.org/x/tools/cmd/gorename. This logic has not worked properly |
| // since the advent of Go modules, and should be deleted too. |
| // |
| // Use gopls instead, either via the Rename LSP method or the "gopls |
| // rename" subcommand. |
| package rename |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/build" |
| "go/format" |
| "go/parser" |
| "go/token" |
| "go/types" |
| "io" |
| "log" |
| "os" |
| "os/exec" |
| "path" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "golang.org/x/tools/go/loader" |
| "golang.org/x/tools/go/types/typeutil" |
| "golang.org/x/tools/refactor/importgraph" |
| "golang.org/x/tools/refactor/satisfy" |
| ) |
| |
| const Usage = `gorename: precise type-safe renaming of identifiers in Go source code. |
| |
| Usage: |
| |
| gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force] |
| |
| You must specify the object (named entity) to rename using the -offset |
| or -from flag. Exactly one must be specified. |
| |
| Flags: |
| |
| -offset specifies the filename and byte offset of an identifier to rename. |
| This form is intended for use by text editors. |
| |
| -from specifies the object to rename using a query notation; |
| This form is intended for interactive use at the command line. |
| A legal -from query has one of the following forms: |
| |
| "encoding/json".Decoder.Decode method of package-level named type |
| (*"encoding/json".Decoder).Decode ditto, alternative syntax |
| "encoding/json".Decoder.buf field of package-level named struct type |
| "encoding/json".HTMLEscape package member (const, func, var, type) |
| "encoding/json".Decoder.Decode::x local object x within a method |
| "encoding/json".HTMLEscape::x local object x within a function |
| "encoding/json"::x object x anywhere within a package |
| json.go::x object x within file json.go |
| |
| Double-quotes must be escaped when writing a shell command. |
| Quotes may be omitted for single-segment import paths such as "fmt". |
| |
| For methods, the parens and '*' on the receiver type are both |
| optional. |
| |
| It is an error if one of the ::x queries matches multiple |
| objects. |
| |
| -to the new name. |
| |
| -force causes the renaming to proceed even if conflicts were reported. |
| The resulting program may be ill-formed, or experience a change |
| in behaviour. |
| |
| WARNING: this flag may even cause the renaming tool to crash. |
| (In due course this bug will be fixed by moving certain |
| analyses into the type-checker.) |
| |
| -d display diffs instead of rewriting files |
| |
| -v enables verbose logging. |
| |
| gorename automatically computes the set of packages that might be |
| affected. For a local renaming, this is just the package specified by |
| -from or -offset, but for a potentially exported name, gorename scans |
| the workspace ($GOROOT and $GOPATH). |
| |
| gorename rejects renamings of concrete methods that would change the |
| assignability relation between types and interfaces. If the interface |
| change was intentional, initiate the renaming at the interface method. |
| |
| gorename rejects any renaming that would create a conflict at the point |
| of declaration, or a reference conflict (ambiguity or shadowing), or |
| anything else that could cause the resulting program not to compile. |
| |
| |
| Examples: |
| |
| $ gorename -offset file.go:#123 -to foo |
| |
| Rename the object whose identifier is at byte offset 123 within file file.go. |
| |
| $ gorename -from '"bytes".Buffer.Len' -to Size |
| |
| Rename the "Len" method of the *bytes.Buffer type to "Size". |
| ` |
| |
| // ---- TODO ---- |
| |
| // Correctness: |
| // - handle dot imports correctly |
| // - document limitations (reflection, 'implements' algorithm). |
| // - sketch a proof of exhaustiveness. |
| |
| // Features: |
| // - support running on packages specified as *.go files on the command line |
| // - support running on programs containing errors (loader.Config.AllowErrors) |
| // - allow users to specify a scope other than "global" (to avoid being |
| // stuck by neglected packages in $GOPATH that don't build). |
| // - support renaming the package clause (no object) |
| // - support renaming an import path (no ident or object) |
| // (requires filesystem + SCM updates). |
| // - detect and reject edits to autogenerated files (cgo, protobufs) |
| // and optionally $GOROOT packages. |
| // - report all conflicts, or at least all qualitatively distinct ones. |
| // Sometimes we stop to avoid redundancy, but |
| // it may give a disproportionate sense of safety in -force mode. |
| // - support renaming all instances of a pattern, e.g. |
| // all receiver vars of a given type, |
| // all local variables of a given type, |
| // all PkgNames for a given package. |
| // - emit JSON output for other editors and tools. |
| |
| var ( |
| // Force enables patching of the source files even if conflicts were reported. |
| // The resulting program may be ill-formed. |
| // It may even cause gorename to crash. TODO(adonovan): fix that. |
| Force bool |
| |
| // Diff causes the tool to display diffs instead of rewriting files. |
| Diff bool |
| |
| // DiffCmd specifies the diff command used by the -d feature. |
| // (The command must accept a -u flag and two filename arguments.) |
| DiffCmd = "diff" |
| |
| // ConflictError is returned by Main when it aborts the renaming due to conflicts. |
| // (It is distinguished because the interesting errors are the conflicts themselves.) |
| ConflictError = errors.New("renaming aborted due to conflicts") |
| |
| // Verbose enables extra logging. |
| Verbose bool |
| ) |
| |
| var stdout io.Writer = os.Stdout |
| |
| type renamer struct { |
| iprog *loader.Program |
| objsToUpdate map[types.Object]bool |
| hadConflicts bool |
| from, to string |
| satisfyConstraints map[satisfy.Constraint]bool |
| packages map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect |
| msets typeutil.MethodSetCache |
| changeMethods bool |
| } |
| |
| var reportError = func(posn token.Position, message string) { |
| fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message) |
| } |
| |
| // importName renames imports of fromPath within the package specified by info. |
| // If fromName is not empty, importName renames only imports as fromName. |
| // If the renaming would lead to a conflict, the file is left unchanged. |
| func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error { |
| if fromName == to { |
| return nil // no-op (e.g. rename x/foo to y/foo) |
| } |
| for _, f := range info.Files { |
| var from types.Object |
| for _, imp := range f.Imports { |
| importPath, _ := strconv.Unquote(imp.Path.Value) |
| importName := path.Base(importPath) |
| if imp.Name != nil { |
| importName = imp.Name.Name |
| } |
| if importPath == fromPath && (fromName == "" || importName == fromName) { |
| from = info.Implicits[imp] |
| break |
| } |
| } |
| if from == nil { |
| continue |
| } |
| r := renamer{ |
| iprog: iprog, |
| objsToUpdate: make(map[types.Object]bool), |
| to: to, |
| packages: map[*types.Package]*loader.PackageInfo{info.Pkg: info}, |
| } |
| r.check(from) |
| if r.hadConflicts { |
| reportError(iprog.Fset.Position(f.Imports[0].Pos()), |
| "skipping update of this file") |
| continue // ignore errors; leave the existing name |
| } |
| if err := r.update(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error { |
| // -- Parse the -from or -offset specifier ---------------------------- |
| |
| if (offsetFlag == "") == (fromFlag == "") { |
| return fmt.Errorf("exactly one of the -from and -offset flags must be specified") |
| } |
| |
| if !isValidIdentifier(to) { |
| return fmt.Errorf("-to %q: not a valid identifier", to) |
| } |
| |
| if Diff { |
| defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile) |
| writeFile = diff |
| } |
| |
| var spec *spec |
| var err error |
| if fromFlag != "" { |
| spec, err = parseFromFlag(ctxt, fromFlag) |
| } else { |
| spec, err = parseOffsetFlag(ctxt, offsetFlag) |
| } |
| if err != nil { |
| return err |
| } |
| |
| if spec.fromName == to { |
| return fmt.Errorf("the old and new names are the same: %s", to) |
| } |
| |
| // -- Load the program consisting of the initial package ------------- |
| |
| iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true}) |
| if err != nil { |
| return err |
| } |
| |
| fromObjects, err := findFromObjects(iprog, spec) |
| if err != nil { |
| return err |
| } |
| |
| // -- Load a larger program, for global renamings --------------------- |
| |
| if requiresGlobalRename(fromObjects, to) { |
| // For a local refactoring, we needn't load more |
| // packages, but if the renaming affects the package's |
| // API, we we must load all packages that depend on the |
| // package defining the object, plus their tests. |
| |
| if Verbose { |
| log.Print("Potentially global renaming; scanning workspace...") |
| } |
| |
| // Scan the workspace and build the import graph. |
| _, rev, errors := importgraph.Build(ctxt) |
| if len(errors) > 0 { |
| // With a large GOPATH tree, errors are inevitable. |
| // Report them but proceed. |
| fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n") |
| for path, err := range errors { |
| fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err) |
| } |
| } |
| |
| // Enumerate the set of potentially affected packages. |
| affectedPackages := make(map[string]bool) |
| for _, obj := range fromObjects { |
| // External test packages are never imported, |
| // so they will never appear in the graph. |
| for path := range rev.Search(obj.Pkg().Path()) { |
| affectedPackages[path] = true |
| } |
| } |
| |
| // TODO(adonovan): allow the user to specify the scope, |
| // or -ignore patterns? Computing the scope when we |
| // don't (yet) support inputs containing errors can make |
| // the tool rather brittle. |
| |
| // Re-load the larger program. |
| iprog, err = loadProgram(ctxt, affectedPackages) |
| if err != nil { |
| return err |
| } |
| |
| fromObjects, err = findFromObjects(iprog, spec) |
| if err != nil { |
| return err |
| } |
| } |
| |
| // -- Do the renaming ------------------------------------------------- |
| |
| r := renamer{ |
| iprog: iprog, |
| objsToUpdate: make(map[types.Object]bool), |
| from: spec.fromName, |
| to: to, |
| packages: make(map[*types.Package]*loader.PackageInfo), |
| } |
| |
| // A renaming initiated at an interface method indicates the |
| // intention to rename abstract and concrete methods as needed |
| // to preserve assignability. |
| for _, obj := range fromObjects { |
| if obj, ok := obj.(*types.Func); ok { |
| recv := obj.Type().(*types.Signature).Recv() |
| if recv != nil && types.IsInterface(recv.Type()) { |
| r.changeMethods = true |
| break |
| } |
| } |
| } |
| |
| // Only the initially imported packages (iprog.Imported) and |
| // their external tests (iprog.Created) should be inspected or |
| // modified, as only they have type-checked functions bodies. |
| // The rest are just dependencies, needed only for package-level |
| // type information. |
| for _, info := range iprog.Imported { |
| r.packages[info.Pkg] = info |
| } |
| for _, info := range iprog.Created { // (tests) |
| r.packages[info.Pkg] = info |
| } |
| |
| for _, from := range fromObjects { |
| r.check(from) |
| } |
| if r.hadConflicts && !Force { |
| return ConflictError |
| } |
| return r.update() |
| } |
| |
| // loadProgram loads the specified set of packages (plus their tests) |
| // and all their dependencies, from source, through the specified build |
| // context. Only packages in pkgs will have their functions bodies typechecked. |
| func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) { |
| conf := loader.Config{ |
| Build: ctxt, |
| ParserMode: parser.ParseComments, |
| |
| // TODO(adonovan): enable this. Requires making a lot of code more robust! |
| AllowErrors: false, |
| } |
| // Optimization: don't type-check the bodies of functions in our |
| // dependencies, since we only need exported package members. |
| conf.TypeCheckFuncBodies = func(p string) bool { |
| return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")] |
| } |
| |
| if Verbose { |
| var list []string |
| for pkg := range pkgs { |
| list = append(list, pkg) |
| } |
| sort.Strings(list) |
| for _, pkg := range list { |
| log.Printf("Loading package: %s", pkg) |
| } |
| } |
| |
| for pkg := range pkgs { |
| conf.ImportWithTests(pkg) |
| } |
| |
| // Ideally we would just return conf.Load() here, but go/types |
| // reports certain "soft" errors that gc does not (Go issue 14596). |
| // As a workaround, we set AllowErrors=true and then duplicate |
| // the loader's error checking but allow soft errors. |
| // It would be nice if the loader API permitted "AllowErrors: soft". |
| conf.AllowErrors = true |
| prog, err := conf.Load() |
| if err != nil { |
| return nil, err |
| } |
| |
| var errpkgs []string |
| // Report hard errors in indirectly imported packages. |
| for _, info := range prog.AllPackages { |
| if containsHardErrors(info.Errors) { |
| errpkgs = append(errpkgs, info.Pkg.Path()) |
| } |
| } |
| if errpkgs != nil { |
| var more string |
| if len(errpkgs) > 3 { |
| more = fmt.Sprintf(" and %d more", len(errpkgs)-3) |
| errpkgs = errpkgs[:3] |
| } |
| return nil, fmt.Errorf("couldn't load packages due to errors: %s%s", |
| strings.Join(errpkgs, ", "), more) |
| } |
| return prog, nil |
| } |
| |
| func containsHardErrors(errors []error) bool { |
| for _, err := range errors { |
| if err, ok := err.(types.Error); ok && err.Soft { |
| continue |
| } |
| return true |
| } |
| return false |
| } |
| |
| // requiresGlobalRename reports whether this renaming could potentially |
| // affect other packages in the Go workspace. |
| func requiresGlobalRename(fromObjects []types.Object, to string) bool { |
| var tfm bool |
| for _, from := range fromObjects { |
| if from.Exported() { |
| return true |
| } |
| switch objectKind(from) { |
| case "type", "field", "method": |
| tfm = true |
| } |
| } |
| if ast.IsExported(to) && tfm { |
| // A global renaming may be necessary even if we're |
| // exporting a previous unexported name, since if it's |
| // the name of a type, field or method, this could |
| // change selections in other packages. |
| // (We include "type" in this list because a type |
| // used as an embedded struct field entails a field |
| // renaming.) |
| return true |
| } |
| return false |
| } |
| |
| // update updates the input files. |
| func (r *renamer) update() error { |
| // We use token.File, not filename, since a file may appear to |
| // belong to multiple packages and be parsed more than once. |
| // token.File captures this distinction; filename does not. |
| |
| var nidents int |
| var filesToUpdate = make(map[*token.File]bool) |
| docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) |
| for _, info := range r.packages { |
| // Mutate the ASTs and note the filenames. |
| for id, obj := range info.Defs { |
| if r.objsToUpdate[obj] { |
| nidents++ |
| id.Name = r.to |
| filesToUpdate[r.iprog.Fset.File(id.Pos())] = true |
| // Perform the rename in doc comments too. |
| if doc := r.docComment(id); doc != nil { |
| for _, comment := range doc.List { |
| comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to) |
| } |
| } |
| } |
| } |
| |
| for id, obj := range info.Uses { |
| if r.objsToUpdate[obj] { |
| nidents++ |
| id.Name = r.to |
| filesToUpdate[r.iprog.Fset.File(id.Pos())] = true |
| } |
| } |
| } |
| |
| // Renaming not supported if cgo files are affected. |
| var generatedFileNames []string |
| for _, info := range r.packages { |
| for _, f := range info.Files { |
| tokenFile := r.iprog.Fset.File(f.Pos()) |
| if filesToUpdate[tokenFile] && generated(f, tokenFile) { |
| generatedFileNames = append(generatedFileNames, tokenFile.Name()) |
| } |
| } |
| } |
| if !Force && len(generatedFileNames) > 0 { |
| return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames) |
| } |
| |
| // Write affected files. |
| var nerrs, npkgs int |
| for _, info := range r.packages { |
| first := true |
| for _, f := range info.Files { |
| tokenFile := r.iprog.Fset.File(f.Pos()) |
| if filesToUpdate[tokenFile] { |
| if first { |
| npkgs++ |
| first = false |
| if Verbose { |
| log.Printf("Updating package %s", info.Pkg.Path()) |
| } |
| } |
| |
| filename := tokenFile.Name() |
| var buf bytes.Buffer |
| if err := format.Node(&buf, r.iprog.Fset, f); err != nil { |
| log.Printf("failed to pretty-print syntax tree: %v", err) |
| nerrs++ |
| continue |
| } |
| if err := writeFile(filename, buf.Bytes()); err != nil { |
| log.Print(err) |
| nerrs++ |
| } |
| } |
| } |
| } |
| if !Diff { |
| fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n", |
| nidents, plural(nidents), |
| len(filesToUpdate), plural(len(filesToUpdate)), |
| npkgs, plural(npkgs)) |
| } |
| if nerrs > 0 { |
| return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs)) |
| } |
| return nil |
| } |
| |
| // docComment returns the doc for an identifier. |
| func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup { |
| _, nodes, _ := r.iprog.PathEnclosingInterval(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 |
| } |
| |
| func plural(n int) string { |
| if n != 1 { |
| return "s" |
| } |
| return "" |
| } |
| |
| // writeFile is a seam for testing and for the -d flag. |
| var writeFile = reallyWriteFile |
| |
| func reallyWriteFile(filename string, content []byte) error { |
| return os.WriteFile(filename, content, 0644) |
| } |
| |
| func diff(filename string, content []byte) error { |
| renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid()) |
| if err := os.WriteFile(renamed, content, 0644); err != nil { |
| return err |
| } |
| defer os.Remove(renamed) |
| |
| diff, err := exec.Command(DiffCmd, "-u", filename, renamed).Output() |
| if len(diff) > 0 { |
| // diff exits with a non-zero status when the files don't match. |
| // Ignore that failure as long as we get output. |
| stdout.Write(diff) |
| return nil |
| } |
| if err != nil { |
| if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { |
| err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) |
| } |
| return fmt.Errorf("computing diff: %v", err) |
| } |
| return nil |
| } |