|  | // 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 | 
|  | } |