...

Source file src/golang.org/x/tools/refactor/rename/rename.go

Documentation: golang.org/x/tools/refactor/rename

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package rename contains the implementation of the 'gorename' command
     6  // whose main function is in golang.org/x/tools/cmd/gorename.
     7  // See the Usage constant for the command documentation.
     8  package rename // import "golang.org/x/tools/refactor/rename"
     9  
    10  import (
    11  	"bytes"
    12  	"errors"
    13  	"fmt"
    14  	"go/ast"
    15  	"go/build"
    16  	"go/format"
    17  	"go/parser"
    18  	"go/token"
    19  	"go/types"
    20  	exec "golang.org/x/sys/execabs"
    21  	"io"
    22  	"io/ioutil"
    23  	"log"
    24  	"os"
    25  	"path"
    26  	"regexp"
    27  	"sort"
    28  	"strconv"
    29  	"strings"
    30  
    31  	"golang.org/x/tools/go/loader"
    32  	"golang.org/x/tools/go/types/typeutil"
    33  	"golang.org/x/tools/refactor/importgraph"
    34  	"golang.org/x/tools/refactor/satisfy"
    35  )
    36  
    37  const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
    38  
    39  Usage:
    40  
    41   gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
    42  
    43  You must specify the object (named entity) to rename using the -offset
    44  or -from flag.  Exactly one must be specified.
    45  
    46  Flags:
    47  
    48  -offset    specifies the filename and byte offset of an identifier to rename.
    49             This form is intended for use by text editors.
    50  
    51  -from      specifies the object to rename using a query notation;
    52             This form is intended for interactive use at the command line.
    53             A legal -from query has one of the following forms:
    54  
    55    "encoding/json".Decoder.Decode        method of package-level named type
    56    (*"encoding/json".Decoder).Decode     ditto, alternative syntax
    57    "encoding/json".Decoder.buf           field of package-level named struct type
    58    "encoding/json".HTMLEscape            package member (const, func, var, type)
    59    "encoding/json".Decoder.Decode::x     local object x within a method
    60    "encoding/json".HTMLEscape::x         local object x within a function
    61    "encoding/json"::x                    object x anywhere within a package
    62    json.go::x                            object x within file json.go
    63  
    64             Double-quotes must be escaped when writing a shell command.
    65             Quotes may be omitted for single-segment import paths such as "fmt".
    66  
    67             For methods, the parens and '*' on the receiver type are both
    68             optional.
    69  
    70             It is an error if one of the ::x queries matches multiple
    71             objects.
    72  
    73  -to        the new name.
    74  
    75  -force     causes the renaming to proceed even if conflicts were reported.
    76             The resulting program may be ill-formed, or experience a change
    77             in behaviour.
    78  
    79             WARNING: this flag may even cause the renaming tool to crash.
    80             (In due course this bug will be fixed by moving certain
    81             analyses into the type-checker.)
    82  
    83  -d         display diffs instead of rewriting files
    84  
    85  -v         enables verbose logging.
    86  
    87  gorename automatically computes the set of packages that might be
    88  affected.  For a local renaming, this is just the package specified by
    89  -from or -offset, but for a potentially exported name, gorename scans
    90  the workspace ($GOROOT and $GOPATH).
    91  
    92  gorename rejects renamings of concrete methods that would change the
    93  assignability relation between types and interfaces. If the interface
    94  change was intentional, initiate the renaming at the interface method.
    95  
    96  gorename rejects any renaming that would create a conflict at the point
    97  of declaration, or a reference conflict (ambiguity or shadowing), or
    98  anything else that could cause the resulting program not to compile.
    99  
   100  
   101  Examples:
   102  
   103  $ gorename -offset file.go:#123 -to foo
   104  
   105    Rename the object whose identifier is at byte offset 123 within file file.go.
   106  
   107  $ gorename -from '"bytes".Buffer.Len' -to Size
   108  
   109    Rename the "Len" method of the *bytes.Buffer type to "Size".
   110  `
   111  
   112  // ---- TODO ----
   113  
   114  // Correctness:
   115  // - handle dot imports correctly
   116  // - document limitations (reflection, 'implements' algorithm).
   117  // - sketch a proof of exhaustiveness.
   118  
   119  // Features:
   120  // - support running on packages specified as *.go files on the command line
   121  // - support running on programs containing errors (loader.Config.AllowErrors)
   122  // - allow users to specify a scope other than "global" (to avoid being
   123  //   stuck by neglected packages in $GOPATH that don't build).
   124  // - support renaming the package clause (no object)
   125  // - support renaming an import path (no ident or object)
   126  //   (requires filesystem + SCM updates).
   127  // - detect and reject edits to autogenerated files (cgo, protobufs)
   128  //   and optionally $GOROOT packages.
   129  // - report all conflicts, or at least all qualitatively distinct ones.
   130  //   Sometimes we stop to avoid redundancy, but
   131  //   it may give a disproportionate sense of safety in -force mode.
   132  // - support renaming all instances of a pattern, e.g.
   133  //   all receiver vars of a given type,
   134  //   all local variables of a given type,
   135  //   all PkgNames for a given package.
   136  // - emit JSON output for other editors and tools.
   137  
   138  var (
   139  	// Force enables patching of the source files even if conflicts were reported.
   140  	// The resulting program may be ill-formed.
   141  	// It may even cause gorename to crash.  TODO(adonovan): fix that.
   142  	Force bool
   143  
   144  	// Diff causes the tool to display diffs instead of rewriting files.
   145  	Diff bool
   146  
   147  	// DiffCmd specifies the diff command used by the -d feature.
   148  	// (The command must accept a -u flag and two filename arguments.)
   149  	DiffCmd = "diff"
   150  
   151  	// ConflictError is returned by Main when it aborts the renaming due to conflicts.
   152  	// (It is distinguished because the interesting errors are the conflicts themselves.)
   153  	ConflictError = errors.New("renaming aborted due to conflicts")
   154  
   155  	// Verbose enables extra logging.
   156  	Verbose bool
   157  )
   158  
   159  var stdout io.Writer = os.Stdout
   160  
   161  type renamer struct {
   162  	iprog              *loader.Program
   163  	objsToUpdate       map[types.Object]bool
   164  	hadConflicts       bool
   165  	from, to           string
   166  	satisfyConstraints map[satisfy.Constraint]bool
   167  	packages           map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect
   168  	msets              typeutil.MethodSetCache
   169  	changeMethods      bool
   170  }
   171  
   172  var reportError = func(posn token.Position, message string) {
   173  	fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message)
   174  }
   175  
   176  // importName renames imports of fromPath within the package specified by info.
   177  // If fromName is not empty, importName renames only imports as fromName.
   178  // If the renaming would lead to a conflict, the file is left unchanged.
   179  func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error {
   180  	if fromName == to {
   181  		return nil // no-op (e.g. rename x/foo to y/foo)
   182  	}
   183  	for _, f := range info.Files {
   184  		var from types.Object
   185  		for _, imp := range f.Imports {
   186  			importPath, _ := strconv.Unquote(imp.Path.Value)
   187  			importName := path.Base(importPath)
   188  			if imp.Name != nil {
   189  				importName = imp.Name.Name
   190  			}
   191  			if importPath == fromPath && (fromName == "" || importName == fromName) {
   192  				from = info.Implicits[imp]
   193  				break
   194  			}
   195  		}
   196  		if from == nil {
   197  			continue
   198  		}
   199  		r := renamer{
   200  			iprog:        iprog,
   201  			objsToUpdate: make(map[types.Object]bool),
   202  			to:           to,
   203  			packages:     map[*types.Package]*loader.PackageInfo{info.Pkg: info},
   204  		}
   205  		r.check(from)
   206  		if r.hadConflicts {
   207  			reportError(iprog.Fset.Position(f.Imports[0].Pos()),
   208  				"skipping update of this file")
   209  			continue // ignore errors; leave the existing name
   210  		}
   211  		if err := r.update(); err != nil {
   212  			return err
   213  		}
   214  	}
   215  	return nil
   216  }
   217  
   218  func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error {
   219  	// -- Parse the -from or -offset specifier ----------------------------
   220  
   221  	if (offsetFlag == "") == (fromFlag == "") {
   222  		return fmt.Errorf("exactly one of the -from and -offset flags must be specified")
   223  	}
   224  
   225  	if !isValidIdentifier(to) {
   226  		return fmt.Errorf("-to %q: not a valid identifier", to)
   227  	}
   228  
   229  	if Diff {
   230  		defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile)
   231  		writeFile = diff
   232  	}
   233  
   234  	var spec *spec
   235  	var err error
   236  	if fromFlag != "" {
   237  		spec, err = parseFromFlag(ctxt, fromFlag)
   238  	} else {
   239  		spec, err = parseOffsetFlag(ctxt, offsetFlag)
   240  	}
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	if spec.fromName == to {
   246  		return fmt.Errorf("the old and new names are the same: %s", to)
   247  	}
   248  
   249  	// -- Load the program consisting of the initial package  -------------
   250  
   251  	iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true})
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	fromObjects, err := findFromObjects(iprog, spec)
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	// -- Load a larger program, for global renamings ---------------------
   262  
   263  	if requiresGlobalRename(fromObjects, to) {
   264  		// For a local refactoring, we needn't load more
   265  		// packages, but if the renaming affects the package's
   266  		// API, we we must load all packages that depend on the
   267  		// package defining the object, plus their tests.
   268  
   269  		if Verbose {
   270  			log.Print("Potentially global renaming; scanning workspace...")
   271  		}
   272  
   273  		// Scan the workspace and build the import graph.
   274  		_, rev, errors := importgraph.Build(ctxt)
   275  		if len(errors) > 0 {
   276  			// With a large GOPATH tree, errors are inevitable.
   277  			// Report them but proceed.
   278  			fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
   279  			for path, err := range errors {
   280  				fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
   281  			}
   282  		}
   283  
   284  		// Enumerate the set of potentially affected packages.
   285  		affectedPackages := make(map[string]bool)
   286  		for _, obj := range fromObjects {
   287  			// External test packages are never imported,
   288  			// so they will never appear in the graph.
   289  			for path := range rev.Search(obj.Pkg().Path()) {
   290  				affectedPackages[path] = true
   291  			}
   292  		}
   293  
   294  		// TODO(adonovan): allow the user to specify the scope,
   295  		// or -ignore patterns?  Computing the scope when we
   296  		// don't (yet) support inputs containing errors can make
   297  		// the tool rather brittle.
   298  
   299  		// Re-load the larger program.
   300  		iprog, err = loadProgram(ctxt, affectedPackages)
   301  		if err != nil {
   302  			return err
   303  		}
   304  
   305  		fromObjects, err = findFromObjects(iprog, spec)
   306  		if err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	// -- Do the renaming -------------------------------------------------
   312  
   313  	r := renamer{
   314  		iprog:        iprog,
   315  		objsToUpdate: make(map[types.Object]bool),
   316  		from:         spec.fromName,
   317  		to:           to,
   318  		packages:     make(map[*types.Package]*loader.PackageInfo),
   319  	}
   320  
   321  	// A renaming initiated at an interface method indicates the
   322  	// intention to rename abstract and concrete methods as needed
   323  	// to preserve assignability.
   324  	for _, obj := range fromObjects {
   325  		if obj, ok := obj.(*types.Func); ok {
   326  			recv := obj.Type().(*types.Signature).Recv()
   327  			if recv != nil && isInterface(recv.Type().Underlying()) {
   328  				r.changeMethods = true
   329  				break
   330  			}
   331  		}
   332  	}
   333  
   334  	// Only the initially imported packages (iprog.Imported) and
   335  	// their external tests (iprog.Created) should be inspected or
   336  	// modified, as only they have type-checked functions bodies.
   337  	// The rest are just dependencies, needed only for package-level
   338  	// type information.
   339  	for _, info := range iprog.Imported {
   340  		r.packages[info.Pkg] = info
   341  	}
   342  	for _, info := range iprog.Created { // (tests)
   343  		r.packages[info.Pkg] = info
   344  	}
   345  
   346  	for _, from := range fromObjects {
   347  		r.check(from)
   348  	}
   349  	if r.hadConflicts && !Force {
   350  		return ConflictError
   351  	}
   352  	return r.update()
   353  }
   354  
   355  // loadProgram loads the specified set of packages (plus their tests)
   356  // and all their dependencies, from source, through the specified build
   357  // context.  Only packages in pkgs will have their functions bodies typechecked.
   358  func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) {
   359  	conf := loader.Config{
   360  		Build:      ctxt,
   361  		ParserMode: parser.ParseComments,
   362  
   363  		// TODO(adonovan): enable this.  Requires making a lot of code more robust!
   364  		AllowErrors: false,
   365  	}
   366  	// Optimization: don't type-check the bodies of functions in our
   367  	// dependencies, since we only need exported package members.
   368  	conf.TypeCheckFuncBodies = func(p string) bool {
   369  		return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")]
   370  	}
   371  
   372  	if Verbose {
   373  		var list []string
   374  		for pkg := range pkgs {
   375  			list = append(list, pkg)
   376  		}
   377  		sort.Strings(list)
   378  		for _, pkg := range list {
   379  			log.Printf("Loading package: %s", pkg)
   380  		}
   381  	}
   382  
   383  	for pkg := range pkgs {
   384  		conf.ImportWithTests(pkg)
   385  	}
   386  
   387  	// Ideally we would just return conf.Load() here, but go/types
   388  	// reports certain "soft" errors that gc does not (Go issue 14596).
   389  	// As a workaround, we set AllowErrors=true and then duplicate
   390  	// the loader's error checking but allow soft errors.
   391  	// It would be nice if the loader API permitted "AllowErrors: soft".
   392  	conf.AllowErrors = true
   393  	prog, err := conf.Load()
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  
   398  	var errpkgs []string
   399  	// Report hard errors in indirectly imported packages.
   400  	for _, info := range prog.AllPackages {
   401  		if containsHardErrors(info.Errors) {
   402  			errpkgs = append(errpkgs, info.Pkg.Path())
   403  		}
   404  	}
   405  	if errpkgs != nil {
   406  		var more string
   407  		if len(errpkgs) > 3 {
   408  			more = fmt.Sprintf(" and %d more", len(errpkgs)-3)
   409  			errpkgs = errpkgs[:3]
   410  		}
   411  		return nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
   412  			strings.Join(errpkgs, ", "), more)
   413  	}
   414  	return prog, nil
   415  }
   416  
   417  func containsHardErrors(errors []error) bool {
   418  	for _, err := range errors {
   419  		if err, ok := err.(types.Error); ok && err.Soft {
   420  			continue
   421  		}
   422  		return true
   423  	}
   424  	return false
   425  }
   426  
   427  // requiresGlobalRename reports whether this renaming could potentially
   428  // affect other packages in the Go workspace.
   429  func requiresGlobalRename(fromObjects []types.Object, to string) bool {
   430  	var tfm bool
   431  	for _, from := range fromObjects {
   432  		if from.Exported() {
   433  			return true
   434  		}
   435  		switch objectKind(from) {
   436  		case "type", "field", "method":
   437  			tfm = true
   438  		}
   439  	}
   440  	if ast.IsExported(to) && tfm {
   441  		// A global renaming may be necessary even if we're
   442  		// exporting a previous unexported name, since if it's
   443  		// the name of a type, field or method, this could
   444  		// change selections in other packages.
   445  		// (We include "type" in this list because a type
   446  		// used as an embedded struct field entails a field
   447  		// renaming.)
   448  		return true
   449  	}
   450  	return false
   451  }
   452  
   453  // update updates the input files.
   454  func (r *renamer) update() error {
   455  	// We use token.File, not filename, since a file may appear to
   456  	// belong to multiple packages and be parsed more than once.
   457  	// token.File captures this distinction; filename does not.
   458  
   459  	var nidents int
   460  	var filesToUpdate = make(map[*token.File]bool)
   461  	docRegexp := regexp.MustCompile(`\b` + r.from + `\b`)
   462  	for _, info := range r.packages {
   463  		// Mutate the ASTs and note the filenames.
   464  		for id, obj := range info.Defs {
   465  			if r.objsToUpdate[obj] {
   466  				nidents++
   467  				id.Name = r.to
   468  				filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
   469  				// Perform the rename in doc comments too.
   470  				if doc := r.docComment(id); doc != nil {
   471  					for _, comment := range doc.List {
   472  						comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to)
   473  					}
   474  				}
   475  			}
   476  		}
   477  
   478  		for id, obj := range info.Uses {
   479  			if r.objsToUpdate[obj] {
   480  				nidents++
   481  				id.Name = r.to
   482  				filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
   483  			}
   484  		}
   485  	}
   486  
   487  	// Renaming not supported if cgo files are affected.
   488  	var generatedFileNames []string
   489  	for _, info := range r.packages {
   490  		for _, f := range info.Files {
   491  			tokenFile := r.iprog.Fset.File(f.Pos())
   492  			if filesToUpdate[tokenFile] && generated(f, tokenFile) {
   493  				generatedFileNames = append(generatedFileNames, tokenFile.Name())
   494  			}
   495  		}
   496  	}
   497  	if !Force && len(generatedFileNames) > 0 {
   498  		return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames)
   499  	}
   500  
   501  	// Write affected files.
   502  	var nerrs, npkgs int
   503  	for _, info := range r.packages {
   504  		first := true
   505  		for _, f := range info.Files {
   506  			tokenFile := r.iprog.Fset.File(f.Pos())
   507  			if filesToUpdate[tokenFile] {
   508  				if first {
   509  					npkgs++
   510  					first = false
   511  					if Verbose {
   512  						log.Printf("Updating package %s", info.Pkg.Path())
   513  					}
   514  				}
   515  
   516  				filename := tokenFile.Name()
   517  				var buf bytes.Buffer
   518  				if err := format.Node(&buf, r.iprog.Fset, f); err != nil {
   519  					log.Printf("failed to pretty-print syntax tree: %v", err)
   520  					nerrs++
   521  					continue
   522  				}
   523  				if err := writeFile(filename, buf.Bytes()); err != nil {
   524  					log.Print(err)
   525  					nerrs++
   526  				}
   527  			}
   528  		}
   529  	}
   530  	if !Diff {
   531  		fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n",
   532  			nidents, plural(nidents),
   533  			len(filesToUpdate), plural(len(filesToUpdate)),
   534  			npkgs, plural(npkgs))
   535  	}
   536  	if nerrs > 0 {
   537  		return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs))
   538  	}
   539  	return nil
   540  }
   541  
   542  // docComment returns the doc for an identifier.
   543  func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup {
   544  	_, nodes, _ := r.iprog.PathEnclosingInterval(id.Pos(), id.End())
   545  	for _, node := range nodes {
   546  		switch decl := node.(type) {
   547  		case *ast.FuncDecl:
   548  			return decl.Doc
   549  		case *ast.Field:
   550  			return decl.Doc
   551  		case *ast.GenDecl:
   552  			return decl.Doc
   553  		// For {Type,Value}Spec, if the doc on the spec is absent,
   554  		// search for the enclosing GenDecl
   555  		case *ast.TypeSpec:
   556  			if decl.Doc != nil {
   557  				return decl.Doc
   558  			}
   559  		case *ast.ValueSpec:
   560  			if decl.Doc != nil {
   561  				return decl.Doc
   562  			}
   563  		case *ast.Ident:
   564  		default:
   565  			return nil
   566  		}
   567  	}
   568  	return nil
   569  }
   570  
   571  func plural(n int) string {
   572  	if n != 1 {
   573  		return "s"
   574  	}
   575  	return ""
   576  }
   577  
   578  // writeFile is a seam for testing and for the -d flag.
   579  var writeFile = reallyWriteFile
   580  
   581  func reallyWriteFile(filename string, content []byte) error {
   582  	return ioutil.WriteFile(filename, content, 0644)
   583  }
   584  
   585  func diff(filename string, content []byte) error {
   586  	renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid())
   587  	if err := ioutil.WriteFile(renamed, content, 0644); err != nil {
   588  		return err
   589  	}
   590  	defer os.Remove(renamed)
   591  
   592  	diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput()
   593  	if len(diff) > 0 {
   594  		// diff exits with a non-zero status when the files don't match.
   595  		// Ignore that failure as long as we get output.
   596  		stdout.Write(diff)
   597  		return nil
   598  	}
   599  	if err != nil {
   600  		return fmt.Errorf("computing diff: %v", err)
   601  	}
   602  	return nil
   603  }
   604  

View as plain text