+++ /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
-}