1
0
mirror of https://github.com/golang/go synced 2024-11-18 14:04:45 -07:00
go/refactor/rename/rename.go
David Symonds 24257c8cd2 tools: add import comments.
Change-Id: Idda6e64580432cb9a731e4ebf4005ee4ceb4202d
Reviewed-on: https://go-review.googlesource.com/1244
Reviewed-by: Andrew Gerrand <adg@golang.org>
2014-12-09 22:42:16 +00:00

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 "golang.org/x/tools/refactor/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
}