// 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 implementation of the 'gorename' command // whose main function is in golang.org/x/tools/cmd/gorename. // See the Usage constant for the command documentation. package rename // import "golang.org/x/tools/refactor/rename" import ( "bytes" "errors" "fmt" "go/ast" "go/build" "go/format" "go/parser" "go/token" "go/types" "io" "io/ioutil" "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 | -offset :#) -to [-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 && isInterface(recv.Type().Underlying()) { 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 ioutil.WriteFile(filename, content, 0644) } func diff(filename string, content []byte) error { renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid()) if err := ioutil.WriteFile(renamed, content, 0644); err != nil { return err } defer os.Remove(renamed) diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput() 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 { return fmt.Errorf("computing diff: %v", err) } return nil }