// 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" "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" "golang.org/x/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).Decode ditto, alternative syntax "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, the parens and '*' on the receiver type are both optional. Double-quotes may be omitted for single-segment import paths such as fmt. They may need to be escaped when writing a shell command. 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) } 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 } // 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 } // 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") } // 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. // // 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()) }