From 74021b417566bf454c6cba504c3114d6e891bb1a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 23 Sep 2014 10:23:04 -0400 Subject: [PATCH] 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 --- cmd/gorename/main.go | 137 ++++++ refactor/rename/check.go | 660 ++++++++++++++++++++++++++++ refactor/rename/rename.el | 92 ++++ refactor/rename/rename.go | 331 ++++++++++++++ refactor/rename/rename_test.go | 767 +++++++++++++++++++++++++++++++++ refactor/rename/spec.go | 532 +++++++++++++++++++++++ refactor/rename/util.go | 152 +++++++ 7 files changed, 2671 insertions(+) create mode 100644 cmd/gorename/main.go create mode 100644 refactor/rename/check.go create mode 100644 refactor/rename/rename.el create mode 100644 refactor/rename/rename.go create mode 100644 refactor/rename/rename_test.go create mode 100644 refactor/rename/spec.go create mode 100644 refactor/rename/util.go diff --git a/cmd/gorename/main.go b/cmd/gorename/main.go new file mode 100644 index 0000000000..aeee13d25e --- /dev/null +++ b/cmd/gorename/main.go @@ -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 | -offset :#) -to [-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) + } +} diff --git a/refactor/rename/check.go b/refactor/rename/check.go new file mode 100644 index 0000000000..09f5d94da7 --- /dev/null +++ b/refactor/rename/check.go @@ -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 +} diff --git a/refactor/rename/rename.el b/refactor/rename/rename.el new file mode 100644 index 0000000000..c31a465396 --- /dev/null +++ b/refactor/rename/rename.el @@ -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)))) diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go new file mode 100644 index 0000000000..5563792cd1 --- /dev/null +++ b/refactor/rename/rename.go @@ -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 +} diff --git a/refactor/rename/rename_test.go b/refactor/rename/rename_test.go new file mode 100644 index 0000000000..ec5b0cc422 --- /dev/null +++ b/refactor/rename/rename_test.go @@ -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 } diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go new file mode 100644 index 0000000000..264b3bd08a --- /dev/null +++ b/refactor/rename/spec.go @@ -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()) +} diff --git a/refactor/rename/util.go b/refactor/rename/util.go new file mode 100644 index 0000000000..07b3767c5f --- /dev/null +++ b/refactor/rename/util.go @@ -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 +}