...

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

View as plain text