blob: 1d8c32c9dc3733dfed913f35bad8f3a820a1bdf0 [file] [log] [blame]
// 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 Usage for flag details.
import (
"bytes"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/go/buildutil"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/internal/typesinternal"
)
// A spec specifies an entity to rename.
//
// It is populated from an -offset flag or -from query;
// see Usage 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
}
// parseFromFlag interprets the "-from" flag value as a renaming specification.
// See Usage in rename.go 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)
}
if strings.HasSuffix(main, ".go") {
// main is "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 {
// main is one of:
// "importpath"
// "importpath".member
// (*"importpath".type).fieldormethod (parens and star optional)
if err := parseObjectSpec(&spec, main); err != nil {
return nil, err
}
}
if spec.searchFor != "" {
spec.fromName = spec.searchFor
}
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
// Sanitize the package.
bp, err := ctxt.Import(spec.pkg, cwd, 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 {
log.Printf("-from spec: %+v", spec)
}
return &spec, nil
}
// parseObjectSpec parses main as one of the non-filename forms of
// object specification.
func parseObjectSpec(spec *spec, main string) error {
// Parse main as a Go expression, albeit a strange one.
e, _ := parser.ParseExpr(main)
if pkg := parseImportPath(e); pkg != "" {
// e.g. bytes or "encoding/json": a package
spec.pkg = pkg
if spec.searchFor == "" {
return fmt.Errorf("-from %q: package import path %q must have a ::name suffix",
main, main)
}
return nil
}
if e, ok := e.(*ast.SelectorExpr); ok {
x := unparen(e.X)
// Strip off star constructor, if any.
if star, ok := x.(*ast.StarExpr); ok {
x = star.X
}
if pkg := parseImportPath(x); pkg != "" {
// package member e.g. "encoding/json".HTMLEscape
spec.pkg = pkg // e.g. "encoding/json"
spec.pkgMember = e.Sel.Name // e.g. "HTMLEscape"
spec.fromName = e.Sel.Name
return nil
}
if x, ok := x.(*ast.SelectorExpr); ok {
// field/method of type e.g. ("encoding/json".Decoder).Decode
y := unparen(x.X)
if pkg := parseImportPath(y); pkg != "" {
spec.pkg = pkg // e.g. "encoding/json"
spec.pkgMember = x.Sel.Name // e.g. "Decoder"
spec.typeMember = e.Sel.Name // e.g. "Decode"
spec.fromName = e.Sel.Name
return nil
}
}
}
return fmt.Errorf("-from %q: invalid expression", main)
}
// parseImportPath returns the import path of the package denoted by e.
// Any import path may be represented as a string literal;
// single-segment import paths (e.g. "bytes") may also be represented as
// ast.Ident. parseImportPath returns "" for all other expressions.
func parseImportPath(e ast.Expr) string {
switch e := e.(type) {
case *ast.Ident:
return e.Name // e.g. bytes
case *ast.BasicLit:
if e.Kind == token.STRING {
pkgname, _ := strconv.Unquote(e.Value)
return pkgname // e.g. "encoding/json"
}
}
return ""
}
// 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.
//
// pkg := iprog.ImportMap[spec.pkg]
// if pkg == nil {
// return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen?
// }
// info := iprog.AllPackages[pkg]
// Workaround: lookup by value.
var info *loader.PackageInfo
var pkg *types.Package
for pkg, info = range iprog.AllPackages {
if pkg.Path() == spec.pkg {
break
}
}
if info == nil {
return nil, fmt.Errorf("package %q was not loaded", spec.pkg)
}
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.FileStart)
if !sameFile(thisFile.Name(), spec.filename) {
continue
}
// This package contains the query file.
if spec.offset != 0 {
// We cannot refactor generated files since position information is invalidated.
if generated(f, thisFile) {
return nil, fmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s", thisFile.Name())
}
// 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",
spec.typeMember, info.Pkg.Path(), tName.Name())
}
if spec.searchFor == "" {
// If it is an embedded field, return the type of the field.
if v, ok := obj.(*types.Var); ok && v.Anonymous() {
if t, ok := typesinternal.Unpointer(v.Type()).(hasTypeName); ok {
return []types.Object{t.Obj()}, nil
}
}
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 types.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:%d",
objectKind(obj), filepath.Base(posn.Filename), posn.Line, posn.Column)
}
return fmt.Errorf("ambiguous specifier %s matches %s",
objects[0].Name(), buf.String())
}
// Matches cgo generated comment as well as the proposed standard:
//
// https://golang.org/s/generatedcode
var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
// generated reports whether ast.File is a generated file.
func generated(f *ast.File, tokenFile *token.File) bool {
// Iterate over the comments in the file
for _, commentGroup := range f.Comments {
for _, comment := range commentGroup.List {
if matched := generatedRx.MatchString(comment.Text); matched {
// Check if comment is at the beginning of the line in source
if pos := tokenFile.Position(comment.Slash); pos.Column == 1 {
return true
}
}
}
}
return false
}