github.com/v2fly/tools@v0.100.0/refactor/rename/rename.go (about)

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