--- /dev/null
+// 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 <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 && 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
+}