| // 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" |
| ) |
| |
| // 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.Pos()) |
| 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.%s", |
| spec.typeMember, typeKind(tName.Type()), 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() { |
| switch t := v.Type().(type) { |
| case *types.Pointer: |
| return []types.Object{t.Elem().(*types.Named).Obj()}, nil |
| case *types.Named: |
| 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 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 |
| } |