mirror of
https://github.com/golang/go
synced 2024-11-18 14:14:46 -07:00
a28cb655e6
Fixes golang/go#9013 LGTM=sameer R=sameer, minux CC=golang-codereviews https://golang.org/cl/185000043
367 lines
10 KiB
Go
367 lines
10 KiB
Go
// 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/refactor/rename.
|
|
// See that package for the command documentation.
|
|
package rename
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/loader"
|
|
"golang.org/x/tools/go/types"
|
|
"golang.org/x/tools/refactor/importgraph"
|
|
"golang.org/x/tools/refactor/satisfy"
|
|
)
|
|
|
|
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
|
|
|
|
// DryRun causes the tool to report conflicts but not update any files.
|
|
DryRun bool
|
|
|
|
// 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
|
|
)
|
|
|
|
type renamer struct {
|
|
iprog *loader.Program
|
|
objsToUpdate map[types.Object]bool
|
|
hadConflicts bool
|
|
to string
|
|
satisfyConstraints map[satisfy.Constraint]bool
|
|
packages map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect
|
|
msets types.MethodSetCache
|
|
changeMethods bool
|
|
}
|
|
|
|
var reportError = func(posn token.Position, message string) {
|
|
fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
fmt.Fprintln(os.Stderr, "Potentially global renaming; scanning workspace...")
|
|
}
|
|
|
|
// Scan the workspace and build the import graph.
|
|
_, rev, errors := importgraph.Build(ctxt)
|
|
if len(errors) > 0 {
|
|
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),
|
|
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
|
|
}
|
|
if DryRun {
|
|
// TODO(adonovan): print the delta?
|
|
return nil
|
|
}
|
|
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,
|
|
SourceImports: true,
|
|
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 {
|
|
fmt.Fprintf(os.Stderr, "Loading package: %s\n", pkg)
|
|
}
|
|
}
|
|
|
|
for pkg := range pkgs {
|
|
if err := conf.ImportWithTests(pkg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return conf.Load()
|
|
}
|
|
|
|
// 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)
|
|
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
|
|
}
|
|
}
|
|
for id, obj := range info.Uses {
|
|
if r.objsToUpdate[obj] {
|
|
nidents++
|
|
id.Name = r.to
|
|
filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(adonovan): don't rewrite cgo + generated 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 {
|
|
fmt.Fprintf(os.Stderr, "Updating package %s\n",
|
|
info.Pkg.Path())
|
|
}
|
|
}
|
|
if err := rewriteFile(r.iprog.Fset, f, tokenFile.Name()); err != nil {
|
|
fmt.Fprintf(os.Stderr, "gorename: %s\n", err)
|
|
nerrs++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintf(os.Stderr, "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
|
|
}
|
|
|
|
func plural(n int) string {
|
|
if n != 1 {
|
|
return "s"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func writeFile(name string, fset *token.FileSet, f *ast.File, mode os.FileMode) error {
|
|
out, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
// assume error includes the filename
|
|
return fmt.Errorf("failed to open file: %s", err)
|
|
}
|
|
|
|
// Oddly, os.OpenFile doesn't preserve all the mode bits, hence
|
|
// this chmod. (We use 0600 above to avoid a brief
|
|
// vulnerability if the user has an insecure umask.)
|
|
os.Chmod(name, mode) // ignore error
|
|
|
|
if err := format.Node(out, fset, f); err != nil {
|
|
out.Close() // ignore error
|
|
return fmt.Errorf("failed to write file: %s", err)
|
|
}
|
|
|
|
return out.Close()
|
|
}
|
|
|
|
var rewriteFile = func(fset *token.FileSet, f *ast.File, orig string) (err error) {
|
|
backup := orig + ".gorename.backup"
|
|
// TODO(adonovan): print packages and filenames in a form useful
|
|
// to editors (so they can reload files).
|
|
if Verbose {
|
|
fmt.Fprintf(os.Stderr, "\t%s\n", orig)
|
|
}
|
|
// save file mode
|
|
var mode os.FileMode = 0666
|
|
if fi, err := os.Stat(orig); err == nil {
|
|
mode = fi.Mode()
|
|
}
|
|
if err := os.Rename(orig, backup); err != nil {
|
|
return fmt.Errorf("failed to make backup %s -> %s: %s",
|
|
orig, filepath.Base(backup), err)
|
|
}
|
|
if err := writeFile(orig, fset, f, mode); err != nil {
|
|
// Restore the file from the backup.
|
|
os.Remove(orig) // ignore error
|
|
os.Rename(backup, orig) // ignore error
|
|
return err
|
|
}
|
|
os.Remove(backup) // ignore error
|
|
return nil
|
|
}
|