mirror of
https://github.com/golang/go
synced 2024-11-18 10:54:40 -07:00
cmd/gorename: a precise, type-aware renaming tool for Go identifiers.
See the usage message in main.go for orientation. To the best of my knowledge, the tool implements all required soundness checks, except: - the dynamic behaviour of reflection is obviously undecidable. - it rejects method renamings that change the "implements" relation. It should probably be more aggressive. - actually it only checks the part of the "implements" relation needed for compilation. Understanding the dynamic behaviour of interfaces is obviously undecidable. - a couple of minor gaps are indicated by TODO comments. Also: - Emacs integration. - tests of all safety checks and (some) successful rewrites. LGTM=dominik.honnef, sameer R=gri, sameer, dominik.honnef CC=golang-codereviews https://golang.org/cl/139150044
This commit is contained in:
parent
aba8625c37
commit
74021b4175
137
cmd/gorename/main.go
Normal file
137
cmd/gorename/main.go
Normal file
@ -0,0 +1,137 @@
|
||||
// The gorename command performs precise type-safe renaming of
|
||||
// identifiers in Go source code. See the -help message or Usage
|
||||
// constant for details.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"code.google.com/p/go.tools/refactor/rename"
|
||||
)
|
||||
|
||||
var (
|
||||
offsetFlag = flag.String("offset", "", "file and byte offset of identifier to be renamed, e.g. 'file.go:#123'. For use by editors.")
|
||||
fromFlag = flag.String("from", "", "identifier to be renamed; see -help for formats")
|
||||
toFlag = flag.String("to", "", "new name for identifier")
|
||||
helpFlag = flag.Bool("help", false, "show usage message")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&rename.Force, "force", false, "proceed, even if conflicts were reported")
|
||||
flag.BoolVar(&rename.DryRun, "dryrun", false, "show the change, but do not apply it")
|
||||
flag.BoolVar(&rename.Verbose, "v", false, "print verbose information")
|
||||
|
||||
// If $GOMAXPROCS isn't set, use the full capacity of the machine.
|
||||
// For small machines, use at least 4 threads.
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
n := runtime.NumCPU()
|
||||
if n < 4 {
|
||||
n = 4
|
||||
}
|
||||
runtime.GOMAXPROCS(n)
|
||||
}
|
||||
}
|
||||
|
||||
const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
|
||||
|
||||
Usage:
|
||||
|
||||
gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
|
||||
|
||||
You must specify the object (named entity) to rename using the -offset
|
||||
or -from flag. Exactly one must be specified.
|
||||
|
||||
Flags:
|
||||
|
||||
-offset specifies the filename and byte offset of an identifier to rename.
|
||||
This form is intended for use by text editors.
|
||||
|
||||
-from specifies the object to rename using a query notation;
|
||||
This form is intended for interactive use at the command line.
|
||||
` + rename.FromFlagUsage + `
|
||||
|
||||
-to the new name.
|
||||
|
||||
-force causes the renaming to proceed even if conflicts were reported.
|
||||
The resulting program may be ill-formed, or experience a change
|
||||
in behaviour.
|
||||
|
||||
WARNING: this flag may even cause the renaming tool to crash.
|
||||
(In due course this bug will be fixed by moving certain
|
||||
analyses into the type-checker.)
|
||||
|
||||
-v enables verbose logging.
|
||||
|
||||
gorename automatically computes the set of packages that might be
|
||||
affected. For a local renaming, this is just the package specified by
|
||||
-from or -offset, but for a potentially exported name, gorename scans
|
||||
the workspace ($GOROOT and $GOPATH).
|
||||
|
||||
gorename rejects any renaming that would create a conflict at the point
|
||||
of declaration, or a reference conflict (ambiguity or shadowing), or
|
||||
anything else that could cause the resulting program not to compile.
|
||||
Currently, it also rejects any method renaming that would change the
|
||||
assignability relation between types and interfaces.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
% gorename -offset file.go:#123 -to foo
|
||||
|
||||
Rename the object whose identifer is at byte offset 123 within file file.go.
|
||||
|
||||
% gorename -from '(bytes.Buffer).Len' -to Size
|
||||
|
||||
Rename the "Len" method of the *bytes.Buffer type to "Size".
|
||||
|
||||
---- TODO ----
|
||||
|
||||
Correctness:
|
||||
- implement remaining safety checks.
|
||||
- handle dot imports correctly
|
||||
- document limitations (reflection, 'implements' guesswork).
|
||||
- sketch a proof of exhaustiveness.
|
||||
|
||||
Features:
|
||||
- support running on programs containing errors (loader.Config.AllowErrors)
|
||||
- allow users to specify a scope other than "global" (to avoid being
|
||||
stuck by neglected packages in $GOPATH that don't build).
|
||||
- support renaming the package clause (no object)
|
||||
- support renaming an import path (no ident or object)
|
||||
(requires filesystem + SCM updates).
|
||||
- detect and reject edits to autogenerated files (cgo, protobufs)
|
||||
and optionally $GOROOT packages.
|
||||
- report all conflicts, or at least all qualitatively distinct ones.
|
||||
Sometimes we stop to avoid redundancy, but
|
||||
it may give a disproportionate sense of safety in -force mode.
|
||||
- support renaming all instances of a pattern, e.g.
|
||||
all receiver vars of a given type,
|
||||
all local variables of a given type,
|
||||
all PkgNames for a given package.
|
||||
- emit JSON output for other editors and tools.
|
||||
- integration support for editors other than Emacs.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if len(flag.Args()) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Error: surplus arguments.\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *helpFlag || (*offsetFlag == "" && *fromFlag == "" && *toFlag == "") {
|
||||
fmt.Println(Usage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rename.Main(&build.Default, *offsetFlag, *fromFlag, *toFlag); err != nil {
|
||||
if err != rename.ConflictError {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s.\n", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
660
refactor/rename/check.go
Normal file
660
refactor/rename/check.go
Normal file
@ -0,0 +1,660 @@
|
||||
// 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
|
||||
|
||||
// This file defines the safety checks for each kind of renaming.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"code.google.com/p/go.tools/go/loader"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
"code.google.com/p/go.tools/refactor/lexical"
|
||||
"code.google.com/p/go.tools/refactor/satisfy"
|
||||
)
|
||||
|
||||
// errorf reports an error (e.g. conflict) and prevents file modification.
|
||||
func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) {
|
||||
r.hadConflicts = true
|
||||
reportError(r.iprog.Fset.Position(pos), fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// check performs safety checks of the renaming of the 'from' object to r.to.
|
||||
func (r *renamer) check(from types.Object) {
|
||||
if r.objsToUpdate[from] {
|
||||
return
|
||||
}
|
||||
r.objsToUpdate[from] = true
|
||||
|
||||
// NB: order of conditions is important.
|
||||
if from_, ok := from.(*types.PkgName); ok {
|
||||
r.checkInFileBlock(from_)
|
||||
} else if from_, ok := from.(*types.Label); ok {
|
||||
r.checkLabel(from_)
|
||||
} else if isPackageLevel(from) {
|
||||
r.checkInPackageBlock(from)
|
||||
} else if v, ok := from.(*types.Var); ok && v.IsField() {
|
||||
r.checkStructField(v)
|
||||
} else if f, ok := from.(*types.Func); ok && f.Type().(*types.Signature).Recv() != nil {
|
||||
r.checkMethod(f)
|
||||
} else if isLocal(from) {
|
||||
r.checkInLocalScope(from)
|
||||
} else {
|
||||
r.errorf(from.Pos(), "unexpected %s object %q (please report a bug)\n",
|
||||
objectKind(from), from)
|
||||
}
|
||||
}
|
||||
|
||||
// checkInFileBlock performs safety checks for renames of objects in the file block,
|
||||
// i.e. imported package names.
|
||||
func (r *renamer) checkInFileBlock(from *types.PkgName) {
|
||||
// Check import name is not "init".
|
||||
if r.to == "init" {
|
||||
r.errorf(from.Pos(), "%q is not a valid imported package name", r.to)
|
||||
}
|
||||
|
||||
// Check for conflicts between file and package block.
|
||||
if prev := from.Pkg().Scope().Lookup(r.to); prev != nil {
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q would conflict",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twith this package member %s",
|
||||
objectKind(prev))
|
||||
return // since checkInPackageBlock would report redundant errors
|
||||
}
|
||||
|
||||
// Check for conflicts in lexical scope.
|
||||
r.checkInLexicalScope(from, r.packages[from.Pkg()])
|
||||
|
||||
// Finally, modify ImportSpec syntax to add or remove the Name as needed.
|
||||
info, path, _ := r.iprog.PathEnclosingInterval(from.Pos(), from.Pos())
|
||||
if from.Imported().Name() == r.to {
|
||||
// ImportSpec.Name not needed
|
||||
path[1].(*ast.ImportSpec).Name = nil
|
||||
} else {
|
||||
// ImportSpec.Name needed
|
||||
if spec := path[1].(*ast.ImportSpec); spec.Name == nil {
|
||||
spec.Name = &ast.Ident{NamePos: spec.Path.Pos(), Name: r.to}
|
||||
info.Defs[spec.Name] = from
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkInPackageBlock performs safety checks for renames of
|
||||
// func/var/const/type objects in the package block.
|
||||
func (r *renamer) checkInPackageBlock(from types.Object) {
|
||||
// Check that there are no references to the name from another
|
||||
// package if the renaming would make it unexported.
|
||||
if ast.IsExported(from.Name()) && !ast.IsExported(r.to) {
|
||||
for pkg, info := range r.packages {
|
||||
if pkg == from.Pkg() {
|
||||
continue
|
||||
}
|
||||
if id := someUse(info, from); id != nil &&
|
||||
!r.checkExport(id, pkg, from) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info := r.packages[from.Pkg()]
|
||||
lexinfo := lexical.Structure(r.iprog.Fset, from.Pkg(), &info.Info, info.Files)
|
||||
|
||||
// Check that in the package block, "init" is a function, and never referenced.
|
||||
if r.to == "init" {
|
||||
kind := objectKind(from)
|
||||
if kind == "func" {
|
||||
// Reject if intra-package references to it exist.
|
||||
if refs := lexinfo.Refs[from]; len(refs) > 0 {
|
||||
r.errorf(from.Pos(),
|
||||
"renaming this func %q to %q would make it a package initializer",
|
||||
from.Name(), r.to)
|
||||
r.errorf(refs[0].Id.Pos(), "\tbut references to it exist")
|
||||
}
|
||||
} else {
|
||||
r.errorf(from.Pos(), "you cannot have a %s at package level named %q",
|
||||
kind, r.to)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicts between package block and all file blocks.
|
||||
for _, f := range info.Files {
|
||||
if prev, b := lexinfo.Blocks[f].Lookup(r.to); b == lexinfo.Blocks[f] {
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q would conflict",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twith this %s",
|
||||
objectKind(prev))
|
||||
return // since checkInPackageBlock would report redundant errors
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicts in lexical scope.
|
||||
if from.Exported() {
|
||||
for _, info := range r.packages {
|
||||
r.checkInLexicalScope(from, info)
|
||||
}
|
||||
} else {
|
||||
r.checkInLexicalScope(from, info)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renamer) checkInLocalScope(from types.Object) {
|
||||
info := r.packages[from.Pkg()]
|
||||
|
||||
// Is this object an implicit local var for a type switch?
|
||||
// Each case has its own var, whose position is the decl of y,
|
||||
// but Ident in that decl does not appear in the Uses map.
|
||||
//
|
||||
// switch y := x.(type) { // Defs[Ident(y)] is undefined
|
||||
// case int: print(y) // Implicits[CaseClause(int)] = Var(y_int)
|
||||
// case string: print(y) // Implicits[CaseClause(string)] = Var(y_string)
|
||||
// }
|
||||
//
|
||||
var isCaseVar bool
|
||||
for syntax, obj := range info.Implicits {
|
||||
if _, ok := syntax.(*ast.CaseClause); ok && obj.Pos() == from.Pos() {
|
||||
isCaseVar = true
|
||||
r.check(obj)
|
||||
}
|
||||
}
|
||||
|
||||
r.checkInLexicalScope(from, info)
|
||||
|
||||
// Finally, if this was a type switch, change the variable y.
|
||||
if isCaseVar {
|
||||
_, path, _ := r.iprog.PathEnclosingInterval(from.Pos(), from.Pos())
|
||||
path[0].(*ast.Ident).Name = r.to // path is [Ident AssignStmt TypeSwitchStmt...]
|
||||
}
|
||||
}
|
||||
|
||||
// checkInLexicalScope performs safety checks that a renaming does not
|
||||
// change the lexical reference structure of the specified package.
|
||||
//
|
||||
// For objects in lexical scope, there are three kinds of conflicts:
|
||||
// same-, sub-, and super-block conflicts. We will illustrate all three
|
||||
// using this example:
|
||||
//
|
||||
// var x int
|
||||
// var z int
|
||||
//
|
||||
// func f(y int) {
|
||||
// print(x)
|
||||
// print(y)
|
||||
// }
|
||||
//
|
||||
// Renaming x to z encounters a SAME-BLOCK CONFLICT, because an object
|
||||
// with the new name already exists, defined in the same lexical block
|
||||
// as the old object.
|
||||
//
|
||||
// Renaming x to y encounters a SUB-BLOCK CONFLICT, because there exists
|
||||
// a reference to x from within (what would become) a hole in its scope.
|
||||
// The definition of y in an (inner) sub-block would cast a shadow in
|
||||
// the scope of the renamed variable.
|
||||
//
|
||||
// Renaming y to x encounters a SUPER-BLOCK CONFLICT. This is the
|
||||
// converse situation: there is an existing definition of the new name
|
||||
// (x) in an (enclosing) super-block, and the renaming would create a
|
||||
// hole in its scope, within which there exist references to it. The
|
||||
// new name casts a shadow in scope of the existing definition of x in
|
||||
// the super-block.
|
||||
//
|
||||
// Removing the old name (and all references to it) is always safe, and
|
||||
// requires no checks.
|
||||
//
|
||||
func (r *renamer) checkInLexicalScope(from types.Object, info *loader.PackageInfo) {
|
||||
lexinfo := lexical.Structure(r.iprog.Fset, info.Pkg, &info.Info, info.Files)
|
||||
|
||||
b := lexinfo.Defs[from] // the block defining the 'from' object
|
||||
if b != nil {
|
||||
to, toBlock := b.Lookup(r.to)
|
||||
if toBlock == b {
|
||||
// same-block conflict
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(to.Pos(), "\tconflicts with %s in same block",
|
||||
objectKind(to))
|
||||
return
|
||||
} else if toBlock != nil {
|
||||
// Check for super-block conflict.
|
||||
// The name r.to is defined in a superblock.
|
||||
// Is that name referenced from within this block?
|
||||
for _, ref := range lexinfo.Refs[to] {
|
||||
if obj, _ := ref.Env.Lookup(from.Name()); obj == from {
|
||||
// super-block conflict
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(ref.Id.Pos(), "\twould shadow this reference")
|
||||
r.errorf(to.Pos(), "\tto the %s declared here",
|
||||
objectKind(to))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for sub-block conflict.
|
||||
// Is there an intervening definition of r.to between
|
||||
// the block defining 'from' and some reference to it?
|
||||
for _, ref := range lexinfo.Refs[from] {
|
||||
// TODO(adonovan): think about dot imports.
|
||||
// (Is b == fromBlock an invariant?)
|
||||
_, fromBlock := ref.Env.Lookup(from.Name())
|
||||
fromDepth := fromBlock.Depth()
|
||||
|
||||
to, toBlock := ref.Env.Lookup(r.to)
|
||||
if to != nil {
|
||||
// sub-block conflict
|
||||
if toBlock.Depth() > fromDepth {
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(ref.Id.Pos(), "\twould cause this reference to become shadowed")
|
||||
r.errorf(to.Pos(), "\tby this intervening %s definition",
|
||||
objectKind(to))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Renaming a type that is used as an embedded field
|
||||
// requires renaming the field too. e.g.
|
||||
// type T int // if we rename this to U..
|
||||
// var s struct {T}
|
||||
// print(s.T) // ...this must change too
|
||||
if _, ok := from.(*types.TypeName); ok {
|
||||
for id, obj := range info.Uses {
|
||||
if obj == from {
|
||||
if field := info.Defs[id]; field != nil {
|
||||
r.check(field)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renamer) checkLabel(label *types.Label) {
|
||||
// Check there are no identical labels in the function's label block.
|
||||
// (Label blocks don't nest, so this is easy.)
|
||||
if prev := label.Parent().Lookup(r.to); prev != nil {
|
||||
r.errorf(label.Pos(), "renaming this label %q to %q", label.Name(), prev.Name())
|
||||
r.errorf(prev.Pos(), "\twould conflict with this one")
|
||||
}
|
||||
}
|
||||
|
||||
// checkStructField checks that the field renaming will not cause
|
||||
// conflicts at its declaration, or ambiguity or changes to any selection.
|
||||
func (r *renamer) checkStructField(from *types.Var) {
|
||||
// Check that the struct declaration is free of field conflicts,
|
||||
// and field/method conflicts.
|
||||
|
||||
// go/types offers no easy way to get from a field (or interface
|
||||
// method) to its declaring struct (or interface), so we must
|
||||
// ascend the AST.
|
||||
info, path, _ := r.iprog.PathEnclosingInterval(from.Pos(), from.Pos())
|
||||
// path is [Ident Field FieldList StructType ... File]. Can't fail.
|
||||
|
||||
// Ascend past parens (unlikely).
|
||||
i := 4
|
||||
for {
|
||||
_, ok := path[i].(*ast.ParenExpr)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if spec, ok := path[i].(*ast.TypeSpec); ok {
|
||||
// This struct is also a named type.
|
||||
// We must check for direct (non-promoted) field/field
|
||||
// and method/field conflicts.
|
||||
named := info.Defs[spec.Name].Type()
|
||||
prev, indices, _ := types.LookupFieldOrMethod(named, true, info.Pkg, r.to)
|
||||
if len(indices) == 1 {
|
||||
r.errorf(from.Pos(), "renaming this field %q to %q",
|
||||
from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twould conflict with this %s",
|
||||
objectKind(prev))
|
||||
return // skip checkSelections to avoid redundant errors
|
||||
}
|
||||
} else {
|
||||
// This struct is not a named type.
|
||||
// We need only check for direct (non-promoted) field/field conflicts.
|
||||
T := info.Types[path[3].(*ast.StructType)].Type.Underlying().(*types.Struct)
|
||||
for i := 0; i < T.NumFields(); i++ {
|
||||
if prev := T.Field(i); prev.Name() == r.to {
|
||||
r.errorf(from.Pos(), "renaming this field %q to %q",
|
||||
from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twould conflict with this field")
|
||||
return // skip checkSelections to avoid redundant errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Renaming an anonymous field requires renaming the type too. e.g.
|
||||
// print(s.T) // if we rename T to U,
|
||||
// type T int // this and
|
||||
// var s struct {T} // this must change too.
|
||||
if from.Anonymous() {
|
||||
if named, ok := from.Type().(*types.Named); ok {
|
||||
r.check(named.Obj())
|
||||
} else if named, ok := deref(from.Type()).(*types.Named); ok {
|
||||
r.check(named.Obj())
|
||||
}
|
||||
}
|
||||
|
||||
// Check integrity of existing (field and method) selections.
|
||||
r.checkSelections(from)
|
||||
}
|
||||
|
||||
// checkSelection checks that all uses and selections that resolve to
|
||||
// the specified object would continue to do so after the renaming.
|
||||
func (r *renamer) checkSelections(from types.Object) {
|
||||
for pkg, info := range r.packages {
|
||||
if id := someUse(info, from); id != nil {
|
||||
if !r.checkExport(id, pkg, from) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for syntax, sel := range info.Selections {
|
||||
// There may be extant selections of only the old
|
||||
// name or only the new name, so we must check both.
|
||||
// (If neither, the renaming is sound.)
|
||||
//
|
||||
// In both cases, we wish to compare the lengths
|
||||
// of the implicit field path (Selection.Index)
|
||||
// to see if the renaming would change it.
|
||||
//
|
||||
// If a selection that resolves to 'from', when renamed,
|
||||
// would yield a path of the same or shorter length,
|
||||
// this indicates ambiguity or a changed referent,
|
||||
// analogous to same- or sub-block lexical conflict.
|
||||
//
|
||||
// If a selection using the name 'to' would
|
||||
// yield a path of the same or shorter length,
|
||||
// this indicates ambiguity or shadowing,
|
||||
// analogous to same- or super-block lexical conflict.
|
||||
|
||||
// TODO(adonovan): fix: derive from Types[syntax.X].Mode
|
||||
// TODO(adonovan): test with pointer, value, addressable value.
|
||||
isAddressable := true
|
||||
|
||||
if sel.Obj() == from {
|
||||
if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), r.to); obj != nil {
|
||||
// Renaming this existing selection of
|
||||
// 'from' may block access to an existing
|
||||
// type member named 'to'.
|
||||
delta := len(indices) - len(sel.Index())
|
||||
if delta > 0 {
|
||||
continue // no ambiguity
|
||||
}
|
||||
r.selectionConflict(from, delta, syntax, obj)
|
||||
return
|
||||
}
|
||||
|
||||
} else if sel.Obj().Name() == r.to {
|
||||
if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), from.Name()); obj == from {
|
||||
// Renaming 'from' may cause this existing
|
||||
// selection of the name 'to' to change
|
||||
// its meaning.
|
||||
delta := len(indices) - len(sel.Index())
|
||||
if delta > 0 {
|
||||
continue // no ambiguity
|
||||
}
|
||||
r.selectionConflict(from, -delta, syntax, sel.Obj())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renamer) selectionConflict(from types.Object, delta int, syntax *ast.SelectorExpr, obj types.Object) {
|
||||
r.errorf(from.Pos(), "renaming this %s %q to %q",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
|
||||
switch {
|
||||
case delta < 0:
|
||||
// analogous to sub-block conflict
|
||||
r.errorf(syntax.Sel.Pos(),
|
||||
"\twould change the referent of this selection")
|
||||
r.errorf(obj.Pos(), "\tto this %s", objectKind(obj))
|
||||
case delta == 0:
|
||||
// analogous to same-block conflict
|
||||
r.errorf(syntax.Sel.Pos(),
|
||||
"\twould make this reference ambiguous")
|
||||
r.errorf(obj.Pos(), "\twith this %s", objectKind(obj))
|
||||
case delta > 0:
|
||||
// analogous to super-block conflict
|
||||
r.errorf(syntax.Sel.Pos(),
|
||||
"\twould shadow this selection")
|
||||
r.errorf(obj.Pos(), "\tto the %s declared here",
|
||||
objectKind(obj))
|
||||
}
|
||||
}
|
||||
|
||||
// checkMethod performs safety checks for renaming a method.
|
||||
// There are three hazards:
|
||||
// - declaration conflicts
|
||||
// - selection ambiguity/changes
|
||||
// - entailed renamings of assignable concrete/interface types (for now, just reject)
|
||||
func (r *renamer) checkMethod(from *types.Func) {
|
||||
// e.g. error.Error
|
||||
if from.Pkg() == nil {
|
||||
r.errorf(from.Pos(), "you cannot rename built-in method %s", from)
|
||||
return
|
||||
}
|
||||
|
||||
// As always, having to support concrete methods with pointer
|
||||
// and non-pointer receivers, and named vs unnamed types with
|
||||
// methods, makes tooling fun.
|
||||
|
||||
// ASSIGNABILITY
|
||||
//
|
||||
// For now, if any method renaming breaks a required
|
||||
// assignability to another type, we reject it.
|
||||
//
|
||||
// TODO(adonovan): probably we should compute the entailed
|
||||
// renamings so that an interface method renaming causes
|
||||
// concrete methods to change too. But which ones?
|
||||
//
|
||||
// There is no correct answer, only heuristics, because Go's
|
||||
// "duck typing" doesn't distinguish intentional from contingent
|
||||
// assignability. There are two obvious approaches:
|
||||
//
|
||||
// (1) Update the minimum set of types to preserve the
|
||||
// assignability of types all syntactic assignments
|
||||
// (incl. implicit ones in calls, returns, sends, etc).
|
||||
// The satisfy.Finder enumerates these.
|
||||
// This is likely to be an underapproximation.
|
||||
//
|
||||
// (2) Update all types that are assignable to/from the changed
|
||||
// type. This requires computing the "implements" relation
|
||||
// for all pairs of types (as godoc and oracle do).
|
||||
// This is likely to be an overapproximation.
|
||||
//
|
||||
// If a concrete type is renamed, we probably do not want to
|
||||
// rename corresponding interfaces; interface renamings should
|
||||
// probably be initiated at the interface. (But what if a
|
||||
// concrete type implements multiple interfaces with the same
|
||||
// method? Then the user is stuck.)
|
||||
//
|
||||
// We need some experience before we decide how to implement this.
|
||||
|
||||
// Check for conflict at point of declaration.
|
||||
// Check to ensure preservation of assignability requirements.
|
||||
recv := from.Type().(*types.Signature).Recv().Type()
|
||||
if isInterface(recv) {
|
||||
// Abstract method
|
||||
|
||||
// declaration
|
||||
prev, _, _ := types.LookupFieldOrMethod(recv, false, from.Pkg(), r.to)
|
||||
if prev != nil {
|
||||
r.errorf(from.Pos(), "renaming this interface method %q to %q",
|
||||
from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twould conflict with this method")
|
||||
return
|
||||
}
|
||||
|
||||
// Check all interfaces that embed this one for
|
||||
// declaration conflicts too.
|
||||
for _, info := range r.packages {
|
||||
// Start with named interface types (better errors)
|
||||
for _, obj := range info.Defs {
|
||||
if obj, ok := obj.(*types.TypeName); ok && isInterface(obj.Type()) {
|
||||
f, _, _ := types.LookupFieldOrMethod(
|
||||
obj.Type(), false, from.Pkg(), from.Name())
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
t, _, _ := types.LookupFieldOrMethod(
|
||||
obj.Type(), false, from.Pkg(), r.to)
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
r.errorf(from.Pos(), "renaming this interface method %q to %q",
|
||||
from.Name(), r.to)
|
||||
r.errorf(t.Pos(), "\twould conflict with this method")
|
||||
r.errorf(obj.Pos(), "\tin named interface type %q", obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Now look at all literal interface types (includes named ones again).
|
||||
for e, tv := range info.Types {
|
||||
if e, ok := e.(*ast.InterfaceType); ok {
|
||||
_ = e
|
||||
_ = tv.Type.(*types.Interface)
|
||||
// TODO(adonovan): implement same check as above.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// assignability
|
||||
for T := range r.findAssignments(recv) {
|
||||
if obj, _, _ := types.LookupFieldOrMethod(T, false, from.Pkg(), from.Name()); obj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
r.errorf(from.Pos(), "renaming this method %q to %q",
|
||||
from.Name(), r.to)
|
||||
var pos token.Pos
|
||||
var other string
|
||||
if named, ok := T.(*types.Named); ok {
|
||||
pos = named.Obj().Pos()
|
||||
other = named.Obj().Name()
|
||||
} else {
|
||||
pos = from.Pos()
|
||||
other = T.String()
|
||||
}
|
||||
r.errorf(pos, "\twould make %s no longer assignable to it", other)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Concrete method
|
||||
|
||||
// declaration
|
||||
prev, indices, _ := types.LookupFieldOrMethod(recv, true, from.Pkg(), r.to)
|
||||
if prev != nil && len(indices) == 1 {
|
||||
r.errorf(from.Pos(), "renaming this method %q to %q",
|
||||
from.Name(), r.to)
|
||||
r.errorf(prev.Pos(), "\twould conflict with this %s",
|
||||
objectKind(prev))
|
||||
return
|
||||
}
|
||||
|
||||
// assignability (of both T and *T)
|
||||
recvBase := deref(recv)
|
||||
for _, R := range []types.Type{recvBase, types.NewPointer(recvBase)} {
|
||||
for I := range r.findAssignments(R) {
|
||||
if obj, _, _ := types.LookupFieldOrMethod(I, true, from.Pkg(), from.Name()); obj == nil {
|
||||
continue
|
||||
}
|
||||
r.errorf(from.Pos(), "renaming this method %q to %q",
|
||||
from.Name(), r.to)
|
||||
var pos token.Pos
|
||||
var iface string
|
||||
if named, ok := I.(*types.Named); ok {
|
||||
pos = named.Obj().Pos()
|
||||
iface = "interface " + named.Obj().Name()
|
||||
} else {
|
||||
pos = from.Pos()
|
||||
iface = I.String()
|
||||
}
|
||||
r.errorf(pos, "\twould make it no longer assignable to %s", iface)
|
||||
return // one is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check integrity of existing (field and method) selections.
|
||||
// We skip this if there were errors above, to avoid redundant errors.
|
||||
r.checkSelections(from)
|
||||
}
|
||||
|
||||
func (r *renamer) checkExport(id *ast.Ident, pkg *types.Package, from types.Object) bool {
|
||||
// Reject cross-package references if r.to is unexported.
|
||||
// (Such references may be qualified identifiers or field/method
|
||||
// selections.)
|
||||
if !ast.IsExported(r.to) && pkg != from.Pkg() {
|
||||
r.errorf(from.Pos(),
|
||||
"renaming this %s %q to %q would make it unexported",
|
||||
objectKind(from), from.Name(), r.to)
|
||||
r.errorf(id.Pos(), "\tbreaking references from packages such as %q",
|
||||
pkg.Path())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// findAssignments returns the set of types to or from which type T is
|
||||
// assigned in the program syntax.
|
||||
func (r *renamer) findAssignments(T types.Type) map[types.Type]bool {
|
||||
if r.satisfyConstraints == nil {
|
||||
// Compute on demand: it's expensive.
|
||||
var f satisfy.Finder
|
||||
for _, info := range r.packages {
|
||||
f.Find(&info.Info, info.Files)
|
||||
}
|
||||
r.satisfyConstraints = f.Result
|
||||
}
|
||||
|
||||
result := make(map[types.Type]bool)
|
||||
for key := range r.satisfyConstraints {
|
||||
// key = (lhs, rhs) where lhs is always an interface.
|
||||
if types.Identical(key.RHS, T) {
|
||||
result[key.LHS] = true
|
||||
}
|
||||
if isInterface(T) && types.Identical(key.LHS, T) {
|
||||
// must check both sides
|
||||
result[key.RHS] = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// -- helpers ----------------------------------------------------------
|
||||
|
||||
// someUse returns an arbitrary use of obj within info.
|
||||
func someUse(info *loader.PackageInfo, obj types.Object) *ast.Ident {
|
||||
for id, o := range info.Uses {
|
||||
if o == obj {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- Plundered from code.google.com/p/go.tools/go/ssa -----------------
|
||||
|
||||
func isInterface(T types.Type) bool {
|
||||
_, ok := T.Underlying().(*types.Interface)
|
||||
return ok
|
||||
}
|
||||
|
||||
func deref(typ types.Type) types.Type {
|
||||
if p, _ := typ.(*types.Pointer); p != nil {
|
||||
return p.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
92
refactor/rename/rename.el
Normal file
92
refactor/rename/rename.el
Normal file
@ -0,0 +1,92 @@
|
||||
;;; 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.
|
||||
;;;
|
||||
;;; Integration of the 'gorename' tool into Emacs.
|
||||
;;;
|
||||
;;; To install:
|
||||
;;; % go get code.google.com/p/go.tools/cmd/gorename
|
||||
;;; % go build code.google.com/p/go.tools/cmd/gorename
|
||||
;;; % mv gorename $HOME/bin/ # or elsewhere on $PATH
|
||||
;;;
|
||||
;;; The go-rename-command variable can be customized to specify an
|
||||
;;; alternative location for the installed command.
|
||||
|
||||
(require 'compile)
|
||||
(require 'go-mode)
|
||||
(require 'thingatpt)
|
||||
|
||||
(defgroup go-rename nil
|
||||
"Options specific to the Go rename."
|
||||
:group 'go)
|
||||
|
||||
(defcustom go-rename-command "gorename"
|
||||
"The `gorename' command; by the default, $PATH is searched."
|
||||
:type 'string
|
||||
:group 'go-rename)
|
||||
|
||||
(defun go-rename (new-name)
|
||||
"Rename the entity denoted by the identifier at point, using
|
||||
the `gorename' tool."
|
||||
(interactive (list (read-string "New name: " (thing-at-point 'symbol))))
|
||||
(if (not buffer-file-name)
|
||||
(error "Cannot use go-rename on a buffer without a file name"))
|
||||
;; It's not sufficient to save the current buffer if modified,
|
||||
;; since if gofmt-before-save is on the before-save-hook,
|
||||
;; saving will disturb the selected region.
|
||||
(if (buffer-modified-p)
|
||||
(error "Please save the current buffer before invoking go-rename"))
|
||||
;; Prompt-save all other modified Go buffers, since they might get written.
|
||||
(save-some-buffers nil #'(lambda ()
|
||||
(and (buffer-file-name)
|
||||
(string= (file-name-extension (buffer-file-name)) ".go"))))
|
||||
(let* ((posflag (format "-offset=%s:#%d"
|
||||
buffer-file-name
|
||||
(1- (go--position-bytes (point)))))
|
||||
(env-vars (go-root-and-paths))
|
||||
(goroot-env (concat "GOROOT=" (car env-vars)))
|
||||
(gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":")))
|
||||
success)
|
||||
(with-current-buffer (get-buffer-create "*go-rename*")
|
||||
(setq buffer-read-only nil)
|
||||
(erase-buffer)
|
||||
(let ((args (list go-rename-command nil t nil posflag "-to" new-name)))
|
||||
;; Log the command to *Messages*, for debugging.
|
||||
(message "Command: %s:" args)
|
||||
(message "Running gorename...")
|
||||
;; Use dynamic binding to modify/restore the environment
|
||||
(setq success (zerop (let ((process-environment (list* goroot-env gopath-env process-environment)))
|
||||
(apply #'call-process args))))
|
||||
(insert "\n")
|
||||
(compilation-mode)
|
||||
(setq compilation-error-screen-columns nil)
|
||||
|
||||
;; On success, print the one-line result in the message bar,
|
||||
;; and hide the *go-rename* buffer.
|
||||
(let ((w (display-buffer (current-buffer))))
|
||||
(if success
|
||||
(progn
|
||||
(message "%s" (go--buffer-string-no-trailing-space))
|
||||
(delete-window w))
|
||||
;; failure
|
||||
(message "gorename exited")
|
||||
(shrink-window-if-larger-than-buffer w)
|
||||
(set-window-point w (point-min)))))))
|
||||
|
||||
;; Reload the modified files, saving line/col.
|
||||
;; (Don't restore the point since the text has changed.)
|
||||
;;
|
||||
;; TODO(adonovan): should we also do this for all other files
|
||||
;; that were updated (the tool can print them)?
|
||||
(let ((line (line-number-at-pos))
|
||||
(col (current-column)))
|
||||
(revert-buffer t t t) ; safe, because we just saved it
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- line))
|
||||
(forward-char col)))
|
||||
|
||||
|
||||
(defun go--buffer-string-no-trailing-space ()
|
||||
(replace-regexp-in-string "[\t\n ]*\\'"
|
||||
""
|
||||
(buffer-substring (point-min) (point-max))))
|
331
refactor/rename/rename.go
Normal file
331
refactor/rename/rename.go
Normal file
@ -0,0 +1,331 @@
|
||||
// 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 code.google.com/p/go.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"
|
||||
|
||||
"code.google.com/p/go.tools/go/loader"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
"code.google.com/p/go.tools/refactor/importgraph"
|
||||
"code.google.com/p/go.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 patch to be displayed but not applied.
|
||||
DryRun bool
|
||||
|
||||
// ConfictError 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
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
// 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(), tokenFile.Name()+".prename"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %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 ""
|
||||
}
|
||||
|
||||
var rewriteFile = func(fset *token.FileSet, f *ast.File, orig, backup string) (err error) {
|
||||
// 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)
|
||||
}
|
||||
if err := os.Rename(orig, backup); err != nil {
|
||||
return fmt.Errorf("failed to make backup %s -> %s: %s",
|
||||
orig, filepath.Base(backup), err)
|
||||
}
|
||||
out, err := os.Create(orig)
|
||||
if err != nil {
|
||||
// assume error includes the filename
|
||||
return fmt.Errorf("failed to open file: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := out.Close(); err == nil {
|
||||
err = closeErr // don't clobber existing error
|
||||
}
|
||||
}()
|
||||
if err := format.Node(out, fset, f); err != nil {
|
||||
return fmt.Errorf("failed to write file: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
767
refactor/rename/rename_test.go
Normal file
767
refactor/rename/rename_test.go
Normal file
@ -0,0 +1,767 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO(adonovan): test reported source positions, somehow.
|
||||
|
||||
func TestConflicts(t *testing.T) {
|
||||
defer func(savedDryRun bool, savedReportError func(token.Position, string)) {
|
||||
DryRun = savedDryRun
|
||||
reportError = savedReportError
|
||||
}(DryRun, reportError)
|
||||
DryRun = true
|
||||
|
||||
var ctxt *build.Context
|
||||
for _, test := range []struct {
|
||||
ctxt *build.Context // nil => use previous
|
||||
offset, from, to string // values of the -offset/-from and -to flags
|
||||
want string // regexp to match conflict errors, or "OK"
|
||||
}{
|
||||
// init() checks
|
||||
{
|
||||
ctxt: fakeContext(map[string][]string{
|
||||
"fmt": {`package fmt; type Stringer interface { String() }`},
|
||||
"main": {`
|
||||
package main
|
||||
|
||||
import foo "fmt"
|
||||
|
||||
var v foo.Stringer
|
||||
|
||||
func f() { v.String(); f() }
|
||||
`,
|
||||
`package main; var w int`},
|
||||
}),
|
||||
from: "main.v", to: "init",
|
||||
want: `you cannot have a var at package level named "init"`,
|
||||
},
|
||||
{
|
||||
from: "main.f", to: "init",
|
||||
want: `renaming this func "f" to "init" would make it a package initializer.*` +
|
||||
`but references to it exist`,
|
||||
},
|
||||
{
|
||||
from: "/go/src/main/0.go::foo", to: "init",
|
||||
want: `"init" is not a valid imported package name`,
|
||||
},
|
||||
|
||||
// Export checks
|
||||
{
|
||||
from: "fmt.Stringer", to: "stringer",
|
||||
want: `renaming this type "Stringer" to "stringer" would make it unexported.*` +
|
||||
`breaking references from packages such as "main"`,
|
||||
},
|
||||
{
|
||||
from: "(fmt.Stringer).String", to: "string",
|
||||
want: `renaming this method "String" to "string" would make it unexported.*` +
|
||||
`breaking references from packages such as "main"`,
|
||||
},
|
||||
|
||||
// Lexical scope checks
|
||||
{
|
||||
// file/package conflict, same file
|
||||
from: "main.v", to: "foo",
|
||||
want: `renaming this var "v" to "foo" would conflict.*` +
|
||||
`with this imported package name`,
|
||||
},
|
||||
{
|
||||
// file/package conflict, same file
|
||||
from: "main::foo", to: "v",
|
||||
want: `renaming this imported package name "foo" to "v" would conflict.*` +
|
||||
`with this package member var`,
|
||||
},
|
||||
{
|
||||
// file/package conflict, different files
|
||||
from: "main.w", to: "foo",
|
||||
want: `renaming this var "w" to "foo" would conflict.*` +
|
||||
`with this imported package name`,
|
||||
},
|
||||
{
|
||||
// file/package conflict, different files
|
||||
from: "main::foo", to: "w",
|
||||
want: `renaming this imported package name "foo" to "w" would conflict.*` +
|
||||
`with this package member var`,
|
||||
},
|
||||
{
|
||||
ctxt: main(`
|
||||
package main
|
||||
|
||||
var x, z int
|
||||
|
||||
func f(y int) {
|
||||
print(x)
|
||||
print(y)
|
||||
}
|
||||
|
||||
func g(w int) {
|
||||
print(x)
|
||||
x := 1
|
||||
print(x)
|
||||
}`),
|
||||
from: "main.x", to: "y",
|
||||
want: `renaming this var "x" to "y".*` +
|
||||
`would cause this reference to become shadowed.*` +
|
||||
`by this intervening var definition`,
|
||||
},
|
||||
{
|
||||
from: "main.g::x", to: "w",
|
||||
want: `renaming this var "x" to "w".*` +
|
||||
`conflicts with var in same block`,
|
||||
},
|
||||
{
|
||||
from: "main.f::y", to: "x",
|
||||
want: `renaming this var "y" to "x".*` +
|
||||
`would shadow this reference.*` +
|
||||
`to the var declared here`,
|
||||
},
|
||||
{
|
||||
from: "main.g::w", to: "x",
|
||||
want: `renaming this var "w" to "x".*` +
|
||||
`conflicts with var in same block`,
|
||||
},
|
||||
{
|
||||
from: "main.z", to: "y", want: "OK",
|
||||
},
|
||||
|
||||
// Label checks
|
||||
{
|
||||
ctxt: main(`
|
||||
package main
|
||||
|
||||
func f() {
|
||||
foo:
|
||||
goto foo
|
||||
bar:
|
||||
goto bar
|
||||
func(x int) {
|
||||
wiz:
|
||||
goto wiz
|
||||
}(0)
|
||||
}
|
||||
`),
|
||||
from: "main.f::foo", to: "bar",
|
||||
want: `renaming this label "foo" to "bar".*` +
|
||||
`would conflict with this one`,
|
||||
},
|
||||
{
|
||||
from: "main.f::foo", to: "wiz", want: "OK",
|
||||
},
|
||||
{
|
||||
from: "main.f::wiz", to: "x", want: "OK",
|
||||
},
|
||||
{
|
||||
from: "main.f::x", to: "wiz", want: "OK",
|
||||
},
|
||||
{
|
||||
from: "main.f::wiz", to: "foo", want: "OK",
|
||||
},
|
||||
|
||||
// Struct fields
|
||||
{
|
||||
ctxt: main(`
|
||||
package main
|
||||
|
||||
type U struct { u int }
|
||||
type V struct { v int }
|
||||
|
||||
func (V) x() {}
|
||||
|
||||
type W (struct {
|
||||
U
|
||||
V
|
||||
w int
|
||||
})
|
||||
|
||||
func f() {
|
||||
var w W
|
||||
print(w.u) // NB: there is no selection of w.v
|
||||
var _ struct { yy, zz int }
|
||||
}
|
||||
`),
|
||||
// field/field conflict in named struct declaration
|
||||
from: "(main.W).U", to: "w",
|
||||
want: `renaming this field "U" to "w".*` +
|
||||
`would conflict with this field`,
|
||||
},
|
||||
{
|
||||
// rename type used as embedded field
|
||||
// => rename field
|
||||
// => field/field conflict
|
||||
// This is an entailed renaming;
|
||||
// it would be nice if we checked source positions.
|
||||
from: "main.U", to: "w",
|
||||
want: `renaming this field "U" to "w".*` +
|
||||
`would conflict with this field`,
|
||||
},
|
||||
{
|
||||
// field/field conflict in unnamed struct declaration
|
||||
from: "main.f::zz", to: "yy",
|
||||
want: `renaming this field "zz" to "yy".*` +
|
||||
`would conflict with this field`,
|
||||
},
|
||||
|
||||
// Now we test both directions of (u,v) (u,w) (v,w) (u,x) (v,x).
|
||||
// Too bad we don't test position info...
|
||||
{
|
||||
// field/field ambiguity at same promotion level ('from' selection)
|
||||
from: "(main.U).u", to: "v",
|
||||
want: `renaming this field "u" to "v".*` +
|
||||
`would make this reference ambiguous.*` +
|
||||
`with this field`,
|
||||
},
|
||||
{
|
||||
// field/field ambiguity at same promotion level ('to' selection)
|
||||
from: "(main.V).v", to: "u",
|
||||
want: `renaming this field "v" to "u".*` +
|
||||
`would make this reference ambiguous.*` +
|
||||
`with this field`,
|
||||
},
|
||||
{
|
||||
// field/method conflict at different promotion level ('from' selection)
|
||||
from: "(main.U).u", to: "w",
|
||||
want: `renaming this field "u" to "w".*` +
|
||||
`would change the referent of this selection.*` +
|
||||
`to this field`,
|
||||
},
|
||||
{
|
||||
// field/field shadowing at different promotion levels ('to' selection)
|
||||
from: "(main.W).w", to: "u",
|
||||
want: `renaming this field "w" to "u".*` +
|
||||
`would shadow this selection.*` +
|
||||
`to the field declared here`,
|
||||
},
|
||||
{
|
||||
from: "(main.V).v", to: "w",
|
||||
want: "OK", // since no selections are made ambiguous
|
||||
},
|
||||
{
|
||||
from: "(main.W).w", to: "v",
|
||||
want: "OK", // since no selections are made ambiguous
|
||||
},
|
||||
{
|
||||
// field/method ambiguity at same promotion level ('from' selection)
|
||||
from: "(main.U).u", to: "x",
|
||||
want: `renaming this field "u" to "x".*` +
|
||||
`would make this reference ambiguous.*` +
|
||||
`with this method`,
|
||||
},
|
||||
{
|
||||
// field/field ambiguity at same promotion level ('to' selection)
|
||||
from: "(main.V).x", to: "u",
|
||||
want: `renaming this method "x" to "u".*` +
|
||||
`would make this reference ambiguous.*` +
|
||||
`with this field`,
|
||||
},
|
||||
{
|
||||
// field/method conflict at named struct declaration
|
||||
from: "(main.V).v", to: "x",
|
||||
want: `renaming this field "v" to "x".*` +
|
||||
`would conflict with this method`,
|
||||
},
|
||||
{
|
||||
// field/method conflict at named struct declaration
|
||||
from: "(main.V).x", to: "v",
|
||||
want: `renaming this method "x" to "v".*` +
|
||||
`would conflict with this field`,
|
||||
},
|
||||
|
||||
// Methods
|
||||
{
|
||||
ctxt: main(`
|
||||
package main
|
||||
type C int
|
||||
func (C) f()
|
||||
func (C) g()
|
||||
type D int
|
||||
func (*D) f()
|
||||
func (*D) g()
|
||||
type I interface { f(); g() }
|
||||
type J interface { I; h() }
|
||||
var _ I = new(D)
|
||||
var _ interface {f()} = C(0)
|
||||
`),
|
||||
from: "(main.I).f", to: "g",
|
||||
want: `renaming this interface method "f" to "g".*` +
|
||||
`would conflict with this method`,
|
||||
},
|
||||
{
|
||||
from: "(main.I).f", to: "h",
|
||||
want: `renaming this interface method "f" to "h".*` +
|
||||
`would conflict with this method.*` +
|
||||
`in named interface type "J"`,
|
||||
},
|
||||
{
|
||||
// type J interface { h; h() } is not a conflict, amusingly.
|
||||
from: "main.I", to: "h",
|
||||
want: `OK`,
|
||||
},
|
||||
{
|
||||
from: "(main.J).h", to: "f",
|
||||
want: `renaming this interface method "h" to "f".*` +
|
||||
`would conflict with this method`,
|
||||
},
|
||||
{
|
||||
from: "(main.C).f", to: "e",
|
||||
want: `renaming this method "f" to "e".*` +
|
||||
`would make it no longer assignable to interface{f..}`,
|
||||
},
|
||||
{
|
||||
from: "(main.D).g", to: "e",
|
||||
want: `renaming this method "g" to "e".*` +
|
||||
`would make it no longer assignable to interface I`,
|
||||
},
|
||||
{
|
||||
from: "(main.I).f", to: "e",
|
||||
want: `renaming this method "f" to "e".*` +
|
||||
`would make \*main.D no longer assignable to it`,
|
||||
},
|
||||
} {
|
||||
var conflicts []string
|
||||
reportError = func(posn token.Position, message string) {
|
||||
conflicts = append(conflicts, message)
|
||||
}
|
||||
if test.ctxt != nil {
|
||||
ctxt = test.ctxt
|
||||
}
|
||||
err := Main(ctxt, test.offset, test.from, test.to)
|
||||
var prefix string
|
||||
if test.offset == "" {
|
||||
prefix = fmt.Sprintf("-from %q -to %q", test.from, test.to)
|
||||
} else {
|
||||
prefix = fmt.Sprintf("-offset %q -to %q", test.offset, test.to)
|
||||
}
|
||||
if err == ConflictError {
|
||||
got := strings.Join(conflicts, "\n")
|
||||
if false {
|
||||
t.Logf("%s: %s", prefix, got)
|
||||
}
|
||||
pattern := "(?s:" + test.want + ")" // enable multi-line matching
|
||||
if !regexp.MustCompile(pattern).MatchString(got) {
|
||||
t.Errorf("%s: conflict does not match pattern:\n"+
|
||||
"Conflict:\t%s\n"+
|
||||
"Pattern: %s",
|
||||
prefix, got, test.want)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("%s: unexpected error: %s", prefix, err)
|
||||
} else if test.want != "OK" {
|
||||
t.Errorf("%s: unexpected success, want conflicts matching:\n%s",
|
||||
prefix, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewrites(t *testing.T) {
|
||||
defer func(savedRewriteFile func(*token.FileSet, *ast.File, string, string) error) {
|
||||
rewriteFile = savedRewriteFile
|
||||
}(rewriteFile)
|
||||
|
||||
var ctxt *build.Context
|
||||
for _, test := range []struct {
|
||||
ctxt *build.Context // nil => use previous
|
||||
offset, from, to string // values of the -from/-offset and -to flags
|
||||
want map[string]string // contents of updated files
|
||||
}{
|
||||
// Elimination of renaming import.
|
||||
{
|
||||
ctxt: fakeContext(map[string][]string{
|
||||
"foo": {`package foo; type T int`},
|
||||
"main": {`package main
|
||||
|
||||
import foo2 "foo"
|
||||
|
||||
var _ foo2.T
|
||||
`},
|
||||
}),
|
||||
from: "main::foo2", to: "foo",
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
import "foo"
|
||||
|
||||
var _ foo.T
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Introduction of renaming import.
|
||||
{
|
||||
ctxt: fakeContext(map[string][]string{
|
||||
"foo": {`package foo; type T int`},
|
||||
"main": {`package main
|
||||
|
||||
import "foo"
|
||||
|
||||
var _ foo.T
|
||||
`},
|
||||
}),
|
||||
offset: "/go/src/main/0.go:#36", to: "foo2", // the "foo" in foo.T
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
import foo2 "foo"
|
||||
|
||||
var _ foo2.T
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Renaming of package-level member.
|
||||
{
|
||||
from: "foo.T", to: "U",
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
import "foo"
|
||||
|
||||
var _ foo.U
|
||||
`,
|
||||
"/go/src/foo/0.go": `package foo
|
||||
|
||||
type U int
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Label renamings.
|
||||
{
|
||||
ctxt: main(`package main
|
||||
func f() {
|
||||
loop:
|
||||
loop := 0
|
||||
go func() {
|
||||
loop:
|
||||
goto loop
|
||||
}()
|
||||
loop++
|
||||
goto loop
|
||||
}
|
||||
`),
|
||||
offset: "/go/src/main/0.go:#25", to: "loop2", // def of outer label "loop"
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
func f() {
|
||||
loop2:
|
||||
loop := 0
|
||||
go func() {
|
||||
loop:
|
||||
goto loop
|
||||
}()
|
||||
loop++
|
||||
goto loop2
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
offset: "/go/src/main/0.go:#70", to: "loop2", // ref to inner label "loop"
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
func f() {
|
||||
loop:
|
||||
loop := 0
|
||||
go func() {
|
||||
loop2:
|
||||
goto loop2
|
||||
}()
|
||||
loop++
|
||||
goto loop
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Renaming of type used as embedded field.
|
||||
{
|
||||
ctxt: main(`package main
|
||||
|
||||
type T int
|
||||
type U struct { T }
|
||||
|
||||
var _ = U{}.T
|
||||
`),
|
||||
from: "main.T", to: "T2",
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
type T2 int
|
||||
type U struct{ T2 }
|
||||
|
||||
var _ = U{}.T2
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Renaming of embedded field.
|
||||
{
|
||||
ctxt: main(`package main
|
||||
|
||||
type T int
|
||||
type U struct { T }
|
||||
|
||||
var _ = U{}.T
|
||||
`),
|
||||
offset: "/go/src/main/0.go:#58", to: "T2", // T in "U{}.T"
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
type T2 int
|
||||
type U struct{ T2 }
|
||||
|
||||
var _ = U{}.T2
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Renaming of pointer embedded field.
|
||||
{
|
||||
ctxt: main(`package main
|
||||
|
||||
type T int
|
||||
type U struct { *T }
|
||||
|
||||
var _ = U{}.T
|
||||
`),
|
||||
offset: "/go/src/main/0.go:#59", to: "T2", // T in "U{}.T"
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
type T2 int
|
||||
type U struct{ *T2 }
|
||||
|
||||
var _ = U{}.T2
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
// Lexical scope tests.
|
||||
{
|
||||
ctxt: main(`package main
|
||||
|
||||
var y int
|
||||
|
||||
func f() {
|
||||
print(y)
|
||||
y := ""
|
||||
print(y)
|
||||
}
|
||||
`),
|
||||
from: "main.y", to: "x",
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
var x int
|
||||
|
||||
func f() {
|
||||
print(x)
|
||||
y := ""
|
||||
print(y)
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
from: "main.f::y", to: "x",
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
var y int
|
||||
|
||||
func f() {
|
||||
print(y)
|
||||
x := ""
|
||||
print(x)
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
// Renaming of typeswitch vars (a corner case).
|
||||
{
|
||||
ctxt: main(`package main
|
||||
|
||||
func f(z interface{}) {
|
||||
switch y := z.(type) {
|
||||
case int:
|
||||
print(y)
|
||||
default:
|
||||
print(y)
|
||||
}
|
||||
}
|
||||
`),
|
||||
offset: "/go/src/main/0.go:#46", to: "x", // def of y
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
func f(z interface{}) {
|
||||
switch x := z.(type) {
|
||||
case int:
|
||||
print(x)
|
||||
default:
|
||||
print(x)
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
{
|
||||
offset: "/go/src/main/0.go:#81", to: "x", // ref of y in case int
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
func f(z interface{}) {
|
||||
switch x := z.(type) {
|
||||
case int:
|
||||
print(x)
|
||||
default:
|
||||
print(x)
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
{
|
||||
offset: "/go/src/main/0.go:#102", to: "x", // ref of y in default case
|
||||
want: map[string]string{
|
||||
"/go/src/main/0.go": `package main
|
||||
|
||||
func f(z interface{}) {
|
||||
switch x := z.(type) {
|
||||
case int:
|
||||
print(x)
|
||||
default:
|
||||
print(x)
|
||||
}
|
||||
}
|
||||
`},
|
||||
},
|
||||
} {
|
||||
if test.ctxt != nil {
|
||||
ctxt = test.ctxt
|
||||
}
|
||||
|
||||
got := make(map[string]string)
|
||||
rewriteFile = func(fset *token.FileSet, f *ast.File, orig, backup string) error {
|
||||
var out bytes.Buffer
|
||||
if err := format.Node(&out, fset, f); err != nil {
|
||||
return err
|
||||
}
|
||||
got[orig] = out.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
err := Main(ctxt, test.offset, test.from, test.to)
|
||||
var prefix string
|
||||
if test.offset == "" {
|
||||
prefix = fmt.Sprintf("-from %q -to %q", test.from, test.to)
|
||||
} else {
|
||||
prefix = fmt.Sprintf("-offset %q -to %q", test.offset, test.to)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %s", prefix, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for file, wantContent := range test.want {
|
||||
gotContent, ok := got[file]
|
||||
delete(got, file)
|
||||
if !ok {
|
||||
t.Errorf("%s: file %s not rewritten", prefix, file)
|
||||
continue
|
||||
}
|
||||
if gotContent != wantContent {
|
||||
t.Errorf("%s: rewritten file %s does not match expectation; got <<<%s>>>\n"+
|
||||
"want <<<%s>>>", prefix, file, gotContent, wantContent)
|
||||
}
|
||||
}
|
||||
// got should now be empty
|
||||
for file := range got {
|
||||
t.Errorf("%s: unexpected rewrite of file %s", prefix, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Plundered/adapted from go/loader/loader_test.go
|
||||
|
||||
// TODO(adonovan): make this into a nice testing utility within go/buildutil.
|
||||
|
||||
// pkgs maps the import path of a fake package to a list of its file contents;
|
||||
// file names are synthesized, e.g. %d.go.
|
||||
func fakeContext(pkgs map[string][]string) *build.Context {
|
||||
ctxt := build.Default // copy
|
||||
ctxt.GOROOT = "/go"
|
||||
ctxt.GOPATH = ""
|
||||
ctxt.IsDir = func(path string) bool {
|
||||
if path == "/go/src" {
|
||||
return true // needed by (*build.Context).SrcDirs
|
||||
}
|
||||
if p := strings.TrimPrefix(path, "/go/src/"); p == path {
|
||||
return false
|
||||
} else {
|
||||
path = p
|
||||
}
|
||||
_, ok := pkgs[path]
|
||||
return ok
|
||||
}
|
||||
ctxt.ReadDir = func(dir string) ([]os.FileInfo, error) {
|
||||
dir = dir[len("/go/src/"):]
|
||||
var fis []os.FileInfo
|
||||
if dir == "" {
|
||||
// Assumes keys of pkgs are single-segment.
|
||||
for p := range pkgs {
|
||||
fis = append(fis, fakeDirInfo(p))
|
||||
}
|
||||
} else {
|
||||
for i := range pkgs[dir] {
|
||||
fis = append(fis, fakeFileInfo(i))
|
||||
}
|
||||
}
|
||||
return fis, nil
|
||||
}
|
||||
ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
|
||||
path = path[len("/go/src/"):]
|
||||
dir, base := filepath.Split(path)
|
||||
dir = filepath.Clean(dir)
|
||||
index, _ := strconv.Atoi(strings.TrimSuffix(base, ".go"))
|
||||
return ioutil.NopCloser(bytes.NewBufferString(pkgs[dir][index])), nil
|
||||
}
|
||||
return &ctxt
|
||||
}
|
||||
|
||||
// helper for single-file main packages with no imports.
|
||||
func main(content string) *build.Context {
|
||||
return fakeContext(map[string][]string{"main": {content}})
|
||||
}
|
||||
|
||||
type fakeFileInfo int
|
||||
|
||||
func (fi fakeFileInfo) Name() string { return fmt.Sprintf("%d.go", fi) }
|
||||
func (fakeFileInfo) Sys() interface{} { return nil }
|
||||
func (fakeFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (fakeFileInfo) IsDir() bool { return false }
|
||||
func (fakeFileInfo) Size() int64 { return 0 }
|
||||
func (fakeFileInfo) Mode() os.FileMode { return 0644 }
|
||||
|
||||
type fakeDirInfo string
|
||||
|
||||
func (fd fakeDirInfo) Name() string { return string(fd) }
|
||||
func (fakeDirInfo) Sys() interface{} { return nil }
|
||||
func (fakeDirInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (fakeDirInfo) IsDir() bool { return true }
|
||||
func (fakeDirInfo) Size() int64 { return 0 }
|
||||
func (fakeDirInfo) Mode() os.FileMode { return 0755 }
|
532
refactor/rename/spec.go
Normal file
532
refactor/rename/spec.go
Normal file
@ -0,0 +1,532 @@
|
||||
// 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
|
||||
|
||||
// This file contains logic related to specifying a renaming: parsing of
|
||||
// the flags as a form of query, and finding the object(s) it denotes.
|
||||
// See FromFlagUsage for details.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/go.tools/go/buildutil"
|
||||
"code.google.com/p/go.tools/go/loader"
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
// A spec specifies an entity to rename.
|
||||
//
|
||||
// It is populated from an -offset flag or -from query; see
|
||||
// FromFlagUsage for the allowed -from query forms.
|
||||
//
|
||||
type spec struct {
|
||||
// pkg is the package containing the position
|
||||
// specified by the -from or -offset flag.
|
||||
// If filename == "", our search for the 'from' entity
|
||||
// is restricted to this package.
|
||||
pkg string
|
||||
|
||||
// The original name of the entity being renamed.
|
||||
// If the query had a ::from component, this is that;
|
||||
// otherwise it's the last segment, e.g.
|
||||
// (encoding/json.Decoder).from
|
||||
// encoding/json.from
|
||||
fromName string
|
||||
|
||||
// -- The remaining fields are private to this file. All are optional. --
|
||||
|
||||
// The query's ::x suffix, if any.
|
||||
searchFor string
|
||||
|
||||
// e.g. "Decoder" in "(encoding/json.Decoder).fieldOrMethod"
|
||||
// or "encoding/json.Decoder
|
||||
pkgMember string
|
||||
|
||||
// e.g. fieldOrMethod in "(encoding/json.Decoder).fieldOrMethod"
|
||||
typeMember string
|
||||
|
||||
// Restricts the query to this file.
|
||||
// Implied by -from="file.go::x" and -offset flags.
|
||||
filename string
|
||||
|
||||
// Byte offset of the 'from' identifier within the file named 'filename'.
|
||||
// -offset mode only.
|
||||
offset int
|
||||
}
|
||||
|
||||
const FromFlagUsage = `
|
||||
A legal -from query has one of the following forms:
|
||||
|
||||
(encoding/json.Decoder).Decode method of package-level named type
|
||||
(encoding/json.Decoder).buf field of package-level named struct type
|
||||
encoding/json.HTMLEscape package member (const, func, var, type)
|
||||
(encoding/json.Decoder).Decode::x local object x within a method
|
||||
encoding/json.HTMLEscape::x local object x within a function
|
||||
encoding/json::x object x anywhere within a package
|
||||
json.go::x object x within file json.go
|
||||
|
||||
For methods attached to a pointer type, the '*' must not be specified.
|
||||
[TODO(adonovan): fix that.]
|
||||
|
||||
It is an error if one of the ::x queries matches multiple objects.
|
||||
`
|
||||
|
||||
// parseFromFlag interprets the "-from" flag value as a renaming specification.
|
||||
// See FromFlagUsage for valid formats.
|
||||
func parseFromFlag(ctxt *build.Context, fromFlag string) (*spec, error) {
|
||||
var spec spec
|
||||
var main string // sans "::x" suffix
|
||||
switch parts := strings.Split(fromFlag, "::"); len(parts) {
|
||||
case 1:
|
||||
main = parts[0]
|
||||
case 2:
|
||||
main = parts[0]
|
||||
spec.searchFor = parts[1]
|
||||
if parts[1] == "" {
|
||||
// error
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("-from %q: invalid identifier specification (see -help for formats)", fromFlag)
|
||||
}
|
||||
|
||||
// main is one of:
|
||||
// filename.go
|
||||
// importpath
|
||||
// importpath.member
|
||||
// (importpath.type).fieldormethod
|
||||
|
||||
if strings.HasSuffix(main, ".go") {
|
||||
// filename.go
|
||||
if spec.searchFor == "" {
|
||||
return nil, fmt.Errorf("-from: filename %q must have a ::name suffix", main)
|
||||
}
|
||||
spec.filename = main
|
||||
if !buildutil.FileExists(ctxt, spec.filename) {
|
||||
return nil, fmt.Errorf("no such file: %s", spec.filename)
|
||||
}
|
||||
// Guess the default package.
|
||||
var err error
|
||||
if spec.pkg, err = guessImportPath(spec.filename, ctxt); err != nil {
|
||||
return nil, fmt.Errorf("-from: couldn't guess package from filename: %s: %s",
|
||||
spec.filename, err)
|
||||
}
|
||||
|
||||
} else if a, b := splitAtLastDot(main); b == "" {
|
||||
// importpath e.g. "encoding/json"
|
||||
if spec.searchFor == "" {
|
||||
return nil, fmt.Errorf("-from %q: package import path %q must have a ::name suffix",
|
||||
main, a)
|
||||
}
|
||||
spec.pkg = a
|
||||
|
||||
} else if strings.HasPrefix(a, "(") && strings.HasSuffix(a, ")") {
|
||||
// field/method of type e.g. (encoding/json.Decoder).Decode
|
||||
c, d := splitAtLastDot(a[1 : len(a)-1])
|
||||
if d == "" {
|
||||
return nil, fmt.Errorf("-from %q: not a package-level named type: %q", a)
|
||||
}
|
||||
spec.pkg = c // e.g. "encoding/json"
|
||||
spec.pkgMember = d // e.g. "Decoder"
|
||||
spec.typeMember = b // e.g. "Decode"
|
||||
spec.fromName = b
|
||||
|
||||
} else {
|
||||
// package member e.g. "encoding/json.HTMLEscape"
|
||||
spec.pkg = a // e.g. "encoding/json"
|
||||
spec.pkgMember = b // e.g. "HTMLEscape"
|
||||
spec.fromName = b
|
||||
}
|
||||
|
||||
if spec.searchFor != "" {
|
||||
spec.fromName = spec.searchFor
|
||||
}
|
||||
|
||||
// Sanitize the package.
|
||||
// TODO(adonovan): test with relative packages. May need loader changes.
|
||||
bp, err := ctxt.Import(spec.pkg, ".", build.FindOnly)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't find package %q", spec.pkg)
|
||||
}
|
||||
spec.pkg = bp.ImportPath
|
||||
|
||||
if !isValidIdentifier(spec.fromName) {
|
||||
return nil, fmt.Errorf("-from: invalid identifier %q", spec.fromName)
|
||||
}
|
||||
|
||||
if Verbose {
|
||||
fmt.Fprintf(os.Stderr, "-from spec: %+v\n", spec)
|
||||
}
|
||||
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// "encoding/json.HTMLEscape" -> ("encoding/json", "HTMLEscape")
|
||||
// "encoding/json" -> ("encoding/json", "")
|
||||
func splitAtLastDot(s string) (string, string) {
|
||||
i := strings.LastIndex(s, ".")
|
||||
if i == -1 {
|
||||
return s, ""
|
||||
}
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
|
||||
// parseOffsetFlag interprets the "-offset" flag value as a renaming specification.
|
||||
func parseOffsetFlag(ctxt *build.Context, offsetFlag string) (*spec, error) {
|
||||
var spec spec
|
||||
// Validate -offset, e.g. file.go:#123
|
||||
parts := strings.Split(offsetFlag, ":#")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("-offset %q: invalid offset specification", offsetFlag)
|
||||
}
|
||||
|
||||
spec.filename = parts[0]
|
||||
if !buildutil.FileExists(ctxt, spec.filename) {
|
||||
return nil, fmt.Errorf("no such file: %s", spec.filename)
|
||||
}
|
||||
// Guess the default package.
|
||||
var err error
|
||||
if spec.pkg, err = guessImportPath(spec.filename, ctxt); err != nil {
|
||||
return nil, fmt.Errorf("couldn't guess package from filename: %s: %s",
|
||||
spec.filename, err)
|
||||
}
|
||||
|
||||
for _, r := range parts[1] {
|
||||
if !isDigit(r) {
|
||||
return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
|
||||
}
|
||||
}
|
||||
spec.offset, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
|
||||
}
|
||||
|
||||
// Parse the file and check there's an identifier at that offset.
|
||||
fset := token.NewFileSet()
|
||||
f, err := buildutil.ParseFile(fset, ctxt, nil, wd, spec.filename, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("-offset %q: cannot parse file: %s", offsetFlag, err)
|
||||
}
|
||||
|
||||
id := identAtOffset(fset, f, spec.offset)
|
||||
if id == nil {
|
||||
return nil, fmt.Errorf("-offset %q: no identifier at this position", offsetFlag)
|
||||
}
|
||||
|
||||
spec.fromName = id.Name
|
||||
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
var wd = func() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic("cannot get working directory: " + err.Error())
|
||||
}
|
||||
return wd
|
||||
}()
|
||||
|
||||
// For source trees built with 'go build', the -from or -offset
|
||||
// spec identifies exactly one initial 'from' object to rename ,
|
||||
// but certain proprietary build systems allow a single file to
|
||||
// appear in multiple packages (e.g. the test package contains a
|
||||
// copy of its library), so there may be multiple objects for
|
||||
// the same source entity.
|
||||
|
||||
func findFromObjects(iprog *loader.Program, spec *spec) ([]types.Object, error) {
|
||||
if spec.filename != "" {
|
||||
return findFromObjectsInFile(iprog, spec)
|
||||
}
|
||||
|
||||
// Search for objects defined in specified package.
|
||||
|
||||
// TODO(adonovan): the iprog.ImportMap has an entry {"main": ...}
|
||||
// for main packages, even though that's not an import path.
|
||||
// Seems like a bug.
|
||||
//
|
||||
// pkgObj := iprog.ImportMap[spec.pkg]
|
||||
// if pkgObj == nil {
|
||||
// return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen?
|
||||
// }
|
||||
|
||||
// Workaround: lookup by value.
|
||||
var pkgObj *types.Package
|
||||
for pkg := range iprog.AllPackages {
|
||||
if pkg.Path() == spec.pkg {
|
||||
pkgObj = pkg
|
||||
break
|
||||
}
|
||||
}
|
||||
info := iprog.AllPackages[pkgObj]
|
||||
|
||||
objects, err := findObjects(info, spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(objects) > 1 {
|
||||
// ambiguous "*" scope query
|
||||
return nil, ambiguityError(iprog.Fset, objects)
|
||||
}
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func findFromObjectsInFile(iprog *loader.Program, spec *spec) ([]types.Object, error) {
|
||||
var fromObjects []types.Object
|
||||
for _, info := range iprog.AllPackages {
|
||||
// restrict to specified filename
|
||||
// NB: under certain proprietary build systems, a given
|
||||
// filename may appear in multiple packages.
|
||||
for _, f := range info.Files {
|
||||
thisFile := iprog.Fset.File(f.Pos())
|
||||
if !sameFile(thisFile.Name(), spec.filename) {
|
||||
continue
|
||||
}
|
||||
// This package contains the query file.
|
||||
|
||||
if spec.offset != 0 {
|
||||
// Search for a specific ident by file/offset.
|
||||
id := identAtOffset(iprog.Fset, f, spec.offset)
|
||||
if id == nil {
|
||||
// can't happen?
|
||||
return nil, fmt.Errorf("identifier not found")
|
||||
}
|
||||
obj := info.Uses[id]
|
||||
if obj == nil {
|
||||
obj = info.Defs[id]
|
||||
if obj == nil {
|
||||
// Ident without Object.
|
||||
|
||||
// Package clause?
|
||||
pos := thisFile.Pos(spec.offset)
|
||||
_, path, _ := iprog.PathEnclosingInterval(pos, pos)
|
||||
if len(path) == 2 { // [Ident File]
|
||||
// TODO(adonovan): support this case.
|
||||
return nil, fmt.Errorf("cannot rename %q: renaming package clauses is not yet supported",
|
||||
path[1].(*ast.File).Name.Name)
|
||||
}
|
||||
|
||||
// Implicit y in "switch y := x.(type) {"?
|
||||
if obj := typeSwitchVar(&info.Info, path); obj != nil {
|
||||
return []types.Object{obj}, nil
|
||||
}
|
||||
|
||||
// Probably a type error.
|
||||
return nil, fmt.Errorf("cannot find object for %q", id.Name)
|
||||
}
|
||||
}
|
||||
if obj.Pkg() == nil {
|
||||
return nil, fmt.Errorf("cannot rename predeclared identifiers (%s)", obj)
|
||||
|
||||
}
|
||||
|
||||
fromObjects = append(fromObjects, obj)
|
||||
} else {
|
||||
// do a package-wide query
|
||||
objects, err := findObjects(info, spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// filter results: only objects defined in thisFile
|
||||
var filtered []types.Object
|
||||
for _, obj := range objects {
|
||||
if iprog.Fset.File(obj.Pos()) == thisFile {
|
||||
filtered = append(filtered, obj)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, fmt.Errorf("no object %q declared in file %s",
|
||||
spec.fromName, spec.filename)
|
||||
} else if len(filtered) > 1 {
|
||||
return nil, ambiguityError(iprog.Fset, filtered)
|
||||
}
|
||||
fromObjects = append(fromObjects, filtered[0])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(fromObjects) == 0 {
|
||||
// can't happen?
|
||||
return nil, fmt.Errorf("file %s was not part of the loaded program", spec.filename)
|
||||
}
|
||||
return fromObjects, nil
|
||||
}
|
||||
|
||||
func typeSwitchVar(info *types.Info, path []ast.Node) types.Object {
|
||||
if len(path) > 3 {
|
||||
// [Ident AssignStmt TypeSwitchStmt...]
|
||||
if sw, ok := path[2].(*ast.TypeSwitchStmt); ok {
|
||||
// choose the first case.
|
||||
if len(sw.Body.List) > 0 {
|
||||
obj := info.Implicits[sw.Body.List[0].(*ast.CaseClause)]
|
||||
if obj != nil {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// On success, findObjects returns the list of objects named
|
||||
// spec.fromName matching the spec. On success, the result has exactly
|
||||
// one element unless spec.searchFor!="", in which case it has at least one
|
||||
// element.
|
||||
//
|
||||
func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) {
|
||||
if spec.pkgMember == "" {
|
||||
if spec.searchFor == "" {
|
||||
panic(spec)
|
||||
}
|
||||
objects := searchDefs(&info.Info, spec.searchFor)
|
||||
if objects == nil {
|
||||
return nil, fmt.Errorf("no object %q declared in package %q",
|
||||
spec.searchFor, info.Pkg.Path())
|
||||
}
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
pkgMember := info.Pkg.Scope().Lookup(spec.pkgMember)
|
||||
if pkgMember == nil {
|
||||
return nil, fmt.Errorf("package %q has no member %q",
|
||||
info.Pkg.Path(), spec.pkgMember)
|
||||
}
|
||||
|
||||
var searchFunc *types.Func
|
||||
if spec.typeMember == "" {
|
||||
// package member
|
||||
if spec.searchFor == "" {
|
||||
return []types.Object{pkgMember}, nil
|
||||
}
|
||||
|
||||
// Search within pkgMember, which must be a function.
|
||||
searchFunc, _ = pkgMember.(*types.Func)
|
||||
if searchFunc == nil {
|
||||
return nil, fmt.Errorf("cannot search for %q within %s %q",
|
||||
spec.searchFor, objectKind(pkgMember), pkgMember)
|
||||
}
|
||||
} else {
|
||||
// field/method of type
|
||||
// e.g. (encoding/json.Decoder).Decode
|
||||
// or ::x within it.
|
||||
|
||||
tName, _ := pkgMember.(*types.TypeName)
|
||||
if tName == nil {
|
||||
return nil, fmt.Errorf("%s.%s is a %s, not a type",
|
||||
info.Pkg.Path(), pkgMember.Name(), objectKind(pkgMember))
|
||||
}
|
||||
|
||||
// search within named type.
|
||||
obj, _, _ := types.LookupFieldOrMethod(tName.Type(), true, info.Pkg, spec.typeMember)
|
||||
if obj == nil {
|
||||
return nil, fmt.Errorf("cannot find field or method %q of %s %s.%s",
|
||||
spec.typeMember, typeKind(tName.Type()), info.Pkg.Path(), tName.Name())
|
||||
}
|
||||
|
||||
if spec.searchFor == "" {
|
||||
return []types.Object{obj}, nil
|
||||
}
|
||||
|
||||
searchFunc, _ = obj.(*types.Func)
|
||||
if searchFunc == nil {
|
||||
return nil, fmt.Errorf("cannot search for local name %q within %s (%s.%s).%s; need a function",
|
||||
spec.searchFor, objectKind(obj), info.Pkg.Path(), tName.Name(),
|
||||
obj.Name())
|
||||
}
|
||||
if isInterface(tName.Type()) {
|
||||
return nil, fmt.Errorf("cannot search for local name %q within abstract method (%s.%s).%s",
|
||||
spec.searchFor, info.Pkg.Path(), tName.Name(), searchFunc.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// -- search within function or method --
|
||||
|
||||
decl := funcDecl(info, searchFunc)
|
||||
if decl == nil {
|
||||
return nil, fmt.Errorf("cannot find syntax for %s", searchFunc) // can't happen?
|
||||
}
|
||||
|
||||
var objects []types.Object
|
||||
for _, obj := range searchDefs(&info.Info, spec.searchFor) {
|
||||
// We use positions, not scopes, to determine whether
|
||||
// the obj is within searchFunc. This is clumsy, but the
|
||||
// alternative, using the types.Scope tree, doesn't
|
||||
// account for non-lexical objects like fields and
|
||||
// interface methods.
|
||||
if decl.Pos() <= obj.Pos() && obj.Pos() < decl.End() && obj != searchFunc {
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
}
|
||||
if objects == nil {
|
||||
return nil, fmt.Errorf("no local definition of %q within %s",
|
||||
spec.searchFor, searchFunc)
|
||||
}
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func funcDecl(info *loader.PackageInfo, fn *types.Func) *ast.FuncDecl {
|
||||
for _, f := range info.Files {
|
||||
for _, d := range f.Decls {
|
||||
if d, ok := d.(*ast.FuncDecl); ok && info.Defs[d.Name] == fn {
|
||||
return d
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchDefs(info *types.Info, name string) []types.Object {
|
||||
var objects []types.Object
|
||||
for id, obj := range info.Defs {
|
||||
if obj == nil {
|
||||
// e.g. blank ident.
|
||||
// TODO(adonovan): but also implicit y in
|
||||
// switch y := x.(type)
|
||||
// Needs some thought.
|
||||
continue
|
||||
}
|
||||
if id.Name == name {
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
}
|
||||
return objects
|
||||
}
|
||||
|
||||
func identAtOffset(fset *token.FileSet, f *ast.File, offset int) *ast.Ident {
|
||||
var found *ast.Ident
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
if id, ok := n.(*ast.Ident); ok {
|
||||
idpos := fset.Position(id.Pos()).Offset
|
||||
if idpos <= offset && offset < idpos+len(id.Name) {
|
||||
found = id
|
||||
}
|
||||
}
|
||||
return found == nil // keep traversing only until found
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
// ambiguityError returns an error describing an ambiguous "*" scope query.
|
||||
func ambiguityError(fset *token.FileSet, objects []types.Object) error {
|
||||
var buf bytes.Buffer
|
||||
for i, obj := range objects {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
posn := fset.Position(obj.Pos())
|
||||
fmt.Fprintf(&buf, "%s at %s:%d",
|
||||
objectKind(obj), filepath.Base(posn.Filename), posn.Column)
|
||||
}
|
||||
return fmt.Errorf("ambiguous specifier %s matches %s",
|
||||
objects[0].Name(), buf.String())
|
||||
}
|
152
refactor/rename/util.go
Normal file
152
refactor/rename/util.go
Normal file
@ -0,0 +1,152 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"code.google.com/p/go.tools/go/types"
|
||||
)
|
||||
|
||||
func objectKind(obj types.Object) string {
|
||||
switch obj := obj.(type) {
|
||||
case *types.PkgName:
|
||||
return "imported package name"
|
||||
case *types.TypeName:
|
||||
return "type"
|
||||
case *types.Var:
|
||||
if obj.IsField() {
|
||||
return "field"
|
||||
}
|
||||
case *types.Func:
|
||||
if obj.Type().(*types.Signature).Recv() != nil {
|
||||
return "method"
|
||||
}
|
||||
}
|
||||
// label, func, var, const
|
||||
return strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types."))
|
||||
}
|
||||
|
||||
func typeKind(T types.Type) string {
|
||||
return strings.ToLower(strings.TrimPrefix(reflect.TypeOf(T.Underlying()).String(), "*types."))
|
||||
}
|
||||
|
||||
// NB: for renamings, blank is not considered valid.
|
||||
func isValidIdentifier(id string) bool {
|
||||
if id == "" || id == "_" {
|
||||
return false
|
||||
}
|
||||
for i, r := range id {
|
||||
if !isLetter(r) && (i == 0 || !isDigit(r)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isLocal reports whether obj is local to some function.
|
||||
// Precondition: not a struct field or interface method.
|
||||
func isLocal(obj types.Object) bool {
|
||||
// [... 5=stmt 4=func 3=file 2=pkg 1=universe]
|
||||
var depth int
|
||||
for scope := obj.Parent(); scope != nil; scope = scope.Parent() {
|
||||
depth++
|
||||
}
|
||||
return depth >= 4
|
||||
}
|
||||
|
||||
func isPackageLevel(obj types.Object) bool {
|
||||
return obj.Pkg().Scope().Lookup(obj.Name()) == obj
|
||||
}
|
||||
|
||||
// -- Plundered from go/scanner: ---------------------------------------
|
||||
|
||||
func isLetter(ch rune) bool {
|
||||
return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch)
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch)
|
||||
}
|
||||
|
||||
// -- Plundered from code.google.com/p/go.tools/oracle -----------------
|
||||
|
||||
// guessImportPath finds the package containing filename, and returns
|
||||
// its import path relative to it.
|
||||
func guessImportPath(filename string, ctxt *build.Context) (importPath string, err error) {
|
||||
// TODO(adonovan): move this to package "buildutil"; factor in common with oracle.
|
||||
// bp, err := buildutil.ContainingPackage(ctxt, wd, filename)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// return bp.ImportPath, nil
|
||||
|
||||
absFile, err := filepath.Abs(filename)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("can't form absolute path of %s", filename)
|
||||
return
|
||||
}
|
||||
absFileDir := segments(filepath.Dir(absFile))
|
||||
|
||||
// Find the innermost directory in $GOPATH that encloses filename.
|
||||
minD := 1024
|
||||
for _, gopathDir := range ctxt.SrcDirs() {
|
||||
// We can assume $GOPATH and $GOROOT dirs are absolute,
|
||||
// thus gopathDir too, and that it exists.
|
||||
d := prefixLen(segments(gopathDir), absFileDir)
|
||||
// If there are multiple matches,
|
||||
// prefer the innermost enclosing directory
|
||||
// (smallest d).
|
||||
if d >= 0 && d < minD {
|
||||
minD = d
|
||||
importPath = strings.Join(absFileDir[len(absFileDir)-minD:], string(os.PathSeparator))
|
||||
}
|
||||
}
|
||||
if importPath == "" {
|
||||
err = fmt.Errorf("can't find package for file %s", filename)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func segments(path string) []string {
|
||||
return strings.Split(path, string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// prefixLen returns the length of the remainder of y if x is a prefix
|
||||
// of y, a negative number otherwise.
|
||||
func prefixLen(x, y []string) int {
|
||||
d := len(y) - len(x)
|
||||
if d >= 0 {
|
||||
for i := range x {
|
||||
if y[i] != x[i] {
|
||||
return -1 // not a prefix
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// sameFile returns true if x and y have the same basename and denote
|
||||
// the same file.
|
||||
//
|
||||
func sameFile(x, y string) bool {
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
if filepath.Base(x) == filepath.Base(y) { // (optimisation)
|
||||
if xi, err := os.Stat(x); err == nil {
|
||||
if yi, err := os.Stat(y); err == nil {
|
||||
return os.SameFile(xi, yi)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Loading…
Reference in New Issue
Block a user