mirror of
https://github.com/golang/go
synced 2024-11-05 21:36:12 -07:00
a69ece7231
...by using buildutil.ContainingPackage instead of guessImportPath. The former should be more portable. (I meant to do this earlier, so this is also a nice cleanup.) LGTM=gri R=gri, sameer CC=golang-codereviews https://golang.org/cl/148050043
533 lines
15 KiB
Go
533 lines
15 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rename
|
|
|
|
// 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)
|
|
}
|
|
|
|
bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spec.pkg = bp.ImportPath
|
|
|
|
} 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)
|
|
}
|
|
|
|
bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
spec.pkg = bp.ImportPath
|
|
|
|
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())
|
|
}
|