1 // Copyright 2014 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
7 // This file contains logic related to specifying a renaming: parsing of
8 // the flags as a form of query, and finding the object(s) it denotes.
9 // See Usage for flag details.
26 "golang.org/x/tools/go/buildutil"
27 "golang.org/x/tools/go/loader"
30 // A spec specifies an entity to rename.
32 // It is populated from an -offset flag or -from query;
33 // see Usage for the allowed -from query forms.
36 // pkg is the package containing the position
37 // specified by the -from or -offset flag.
38 // If filename == "", our search for the 'from' entity
39 // is restricted to this package.
42 // The original name of the entity being renamed.
43 // If the query had a ::from component, this is that;
44 // otherwise it's the last segment, e.g.
45 // (encoding/json.Decoder).from
49 // -- The remaining fields are private to this file. All are optional. --
51 // The query's ::x suffix, if any.
54 // e.g. "Decoder" in "(encoding/json.Decoder).fieldOrMethod"
55 // or "encoding/json.Decoder
58 // e.g. fieldOrMethod in "(encoding/json.Decoder).fieldOrMethod"
61 // Restricts the query to this file.
62 // Implied by -from="file.go::x" and -offset flags.
65 // Byte offset of the 'from' identifier within the file named 'filename'.
70 // parseFromFlag interprets the "-from" flag value as a renaming specification.
71 // See Usage in rename.go for valid formats.
72 func parseFromFlag(ctxt *build.Context, fromFlag string) (*spec, error) {
74 var main string // sans "::x" suffix
75 switch parts := strings.Split(fromFlag, "::"); len(parts) {
80 spec.searchFor = parts[1]
85 return nil, fmt.Errorf("-from %q: invalid identifier specification (see -help for formats)", fromFlag)
88 if strings.HasSuffix(main, ".go") {
89 // main is "filename.go"
90 if spec.searchFor == "" {
91 return nil, fmt.Errorf("-from: filename %q must have a ::name suffix", main)
94 if !buildutil.FileExists(ctxt, spec.filename) {
95 return nil, fmt.Errorf("no such file: %s", spec.filename)
98 bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
102 spec.pkg = bp.ImportPath
107 // "importpath".member
108 // (*"importpath".type).fieldormethod (parens and star optional)
109 if err := parseObjectSpec(&spec, main); err != nil {
114 if spec.searchFor != "" {
115 spec.fromName = spec.searchFor
118 cwd, err := os.Getwd()
123 // Sanitize the package.
124 bp, err := ctxt.Import(spec.pkg, cwd, build.FindOnly)
126 return nil, fmt.Errorf("can't find package %q", spec.pkg)
128 spec.pkg = bp.ImportPath
130 if !isValidIdentifier(spec.fromName) {
131 return nil, fmt.Errorf("-from: invalid identifier %q", spec.fromName)
135 log.Printf("-from spec: %+v", spec)
141 // parseObjectSpec parses main as one of the non-filename forms of
142 // object specification.
143 func parseObjectSpec(spec *spec, main string) error {
144 // Parse main as a Go expression, albeit a strange one.
145 e, _ := parser.ParseExpr(main)
147 if pkg := parseImportPath(e); pkg != "" {
148 // e.g. bytes or "encoding/json": a package
150 if spec.searchFor == "" {
151 return fmt.Errorf("-from %q: package import path %q must have a ::name suffix",
157 if e, ok := e.(*ast.SelectorExpr); ok {
160 // Strip off star constructor, if any.
161 if star, ok := x.(*ast.StarExpr); ok {
165 if pkg := parseImportPath(x); pkg != "" {
166 // package member e.g. "encoding/json".HTMLEscape
167 spec.pkg = pkg // e.g. "encoding/json"
168 spec.pkgMember = e.Sel.Name // e.g. "HTMLEscape"
169 spec.fromName = e.Sel.Name
173 if x, ok := x.(*ast.SelectorExpr); ok {
174 // field/method of type e.g. ("encoding/json".Decoder).Decode
176 if pkg := parseImportPath(y); pkg != "" {
177 spec.pkg = pkg // e.g. "encoding/json"
178 spec.pkgMember = x.Sel.Name // e.g. "Decoder"
179 spec.typeMember = e.Sel.Name // e.g. "Decode"
180 spec.fromName = e.Sel.Name
186 return fmt.Errorf("-from %q: invalid expression", main)
189 // parseImportPath returns the import path of the package denoted by e.
190 // Any import path may be represented as a string literal;
191 // single-segment import paths (e.g. "bytes") may also be represented as
192 // ast.Ident. parseImportPath returns "" for all other expressions.
193 func parseImportPath(e ast.Expr) string {
194 switch e := e.(type) {
196 return e.Name // e.g. bytes
199 if e.Kind == token.STRING {
200 pkgname, _ := strconv.Unquote(e.Value)
201 return pkgname // e.g. "encoding/json"
207 // parseOffsetFlag interprets the "-offset" flag value as a renaming specification.
208 func parseOffsetFlag(ctxt *build.Context, offsetFlag string) (*spec, error) {
210 // Validate -offset, e.g. file.go:#123
211 parts := strings.Split(offsetFlag, ":#")
213 return nil, fmt.Errorf("-offset %q: invalid offset specification", offsetFlag)
216 spec.filename = parts[0]
217 if !buildutil.FileExists(ctxt, spec.filename) {
218 return nil, fmt.Errorf("no such file: %s", spec.filename)
221 bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename)
225 spec.pkg = bp.ImportPath
227 for _, r := range parts[1] {
229 return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
232 spec.offset, err = strconv.Atoi(parts[1])
234 return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag)
237 // Parse the file and check there's an identifier at that offset.
238 fset := token.NewFileSet()
239 f, err := buildutil.ParseFile(fset, ctxt, nil, wd, spec.filename, parser.ParseComments)
241 return nil, fmt.Errorf("-offset %q: cannot parse file: %s", offsetFlag, err)
244 id := identAtOffset(fset, f, spec.offset)
246 return nil, fmt.Errorf("-offset %q: no identifier at this position", offsetFlag)
249 spec.fromName = id.Name
254 var wd = func() string {
255 wd, err := os.Getwd()
257 panic("cannot get working directory: " + err.Error())
262 // For source trees built with 'go build', the -from or -offset
263 // spec identifies exactly one initial 'from' object to rename ,
264 // but certain proprietary build systems allow a single file to
265 // appear in multiple packages (e.g. the test package contains a
266 // copy of its library), so there may be multiple objects for
267 // the same source entity.
269 func findFromObjects(iprog *loader.Program, spec *spec) ([]types.Object, error) {
270 if spec.filename != "" {
271 return findFromObjectsInFile(iprog, spec)
274 // Search for objects defined in specified package.
276 // TODO(adonovan): the iprog.ImportMap has an entry {"main": ...}
277 // for main packages, even though that's not an import path.
280 // pkg := iprog.ImportMap[spec.pkg]
282 // return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen?
284 // info := iprog.AllPackages[pkg]
286 // Workaround: lookup by value.
287 var info *loader.PackageInfo
288 var pkg *types.Package
289 for pkg, info = range iprog.AllPackages {
290 if pkg.Path() == spec.pkg {
295 return nil, fmt.Errorf("package %q was not loaded", spec.pkg)
298 objects, err := findObjects(info, spec)
302 if len(objects) > 1 {
303 // ambiguous "*" scope query
304 return nil, ambiguityError(iprog.Fset, objects)
309 func findFromObjectsInFile(iprog *loader.Program, spec *spec) ([]types.Object, error) {
310 var fromObjects []types.Object
311 for _, info := range iprog.AllPackages {
312 // restrict to specified filename
313 // NB: under certain proprietary build systems, a given
314 // filename may appear in multiple packages.
315 for _, f := range info.Files {
316 thisFile := iprog.Fset.File(f.Pos())
317 if !sameFile(thisFile.Name(), spec.filename) {
320 // This package contains the query file.
322 if spec.offset != 0 {
323 // We cannot refactor generated files since position information is invalidated.
324 if generated(f, thisFile) {
325 return nil, fmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s", thisFile.Name())
328 // Search for a specific ident by file/offset.
329 id := identAtOffset(iprog.Fset, f, spec.offset)
332 return nil, fmt.Errorf("identifier not found")
338 // Ident without Object.
341 pos := thisFile.Pos(spec.offset)
342 _, path, _ := iprog.PathEnclosingInterval(pos, pos)
343 if len(path) == 2 { // [Ident File]
344 // TODO(adonovan): support this case.
345 return nil, fmt.Errorf("cannot rename %q: renaming package clauses is not yet supported",
346 path[1].(*ast.File).Name.Name)
349 // Implicit y in "switch y := x.(type) {"?
350 if obj := typeSwitchVar(&info.Info, path); obj != nil {
351 return []types.Object{obj}, nil
354 // Probably a type error.
355 return nil, fmt.Errorf("cannot find object for %q", id.Name)
358 if obj.Pkg() == nil {
359 return nil, fmt.Errorf("cannot rename predeclared identifiers (%s)", obj)
363 fromObjects = append(fromObjects, obj)
365 // do a package-wide query
366 objects, err := findObjects(info, spec)
371 // filter results: only objects defined in thisFile
372 var filtered []types.Object
373 for _, obj := range objects {
374 if iprog.Fset.File(obj.Pos()) == thisFile {
375 filtered = append(filtered, obj)
378 if len(filtered) == 0 {
379 return nil, fmt.Errorf("no object %q declared in file %s",
380 spec.fromName, spec.filename)
381 } else if len(filtered) > 1 {
382 return nil, ambiguityError(iprog.Fset, filtered)
384 fromObjects = append(fromObjects, filtered[0])
389 if len(fromObjects) == 0 {
391 return nil, fmt.Errorf("file %s was not part of the loaded program", spec.filename)
393 return fromObjects, nil
396 func typeSwitchVar(info *types.Info, path []ast.Node) types.Object {
398 // [Ident AssignStmt TypeSwitchStmt...]
399 if sw, ok := path[2].(*ast.TypeSwitchStmt); ok {
400 // choose the first case.
401 if len(sw.Body.List) > 0 {
402 obj := info.Implicits[sw.Body.List[0].(*ast.CaseClause)]
412 // On success, findObjects returns the list of objects named
413 // spec.fromName matching the spec. On success, the result has exactly
414 // one element unless spec.searchFor!="", in which case it has at least one
417 func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) {
418 if spec.pkgMember == "" {
419 if spec.searchFor == "" {
422 objects := searchDefs(&info.Info, spec.searchFor)
424 return nil, fmt.Errorf("no object %q declared in package %q",
425 spec.searchFor, info.Pkg.Path())
430 pkgMember := info.Pkg.Scope().Lookup(spec.pkgMember)
431 if pkgMember == nil {
432 return nil, fmt.Errorf("package %q has no member %q",
433 info.Pkg.Path(), spec.pkgMember)
436 var searchFunc *types.Func
437 if spec.typeMember == "" {
439 if spec.searchFor == "" {
440 return []types.Object{pkgMember}, nil
443 // Search within pkgMember, which must be a function.
444 searchFunc, _ = pkgMember.(*types.Func)
445 if searchFunc == nil {
446 return nil, fmt.Errorf("cannot search for %q within %s %q",
447 spec.searchFor, objectKind(pkgMember), pkgMember)
450 // field/method of type
451 // e.g. (encoding/json.Decoder).Decode
454 tName, _ := pkgMember.(*types.TypeName)
456 return nil, fmt.Errorf("%s.%s is a %s, not a type",
457 info.Pkg.Path(), pkgMember.Name(), objectKind(pkgMember))
460 // search within named type.
461 obj, _, _ := types.LookupFieldOrMethod(tName.Type(), true, info.Pkg, spec.typeMember)
463 return nil, fmt.Errorf("cannot find field or method %q of %s %s.%s",
464 spec.typeMember, typeKind(tName.Type()), info.Pkg.Path(), tName.Name())
467 if spec.searchFor == "" {
468 // If it is an embedded field, return the type of the field.
469 if v, ok := obj.(*types.Var); ok && v.Anonymous() {
470 switch t := v.Type().(type) {
472 return []types.Object{t.Elem().(*types.Named).Obj()}, nil
474 return []types.Object{t.Obj()}, nil
477 return []types.Object{obj}, nil
480 searchFunc, _ = obj.(*types.Func)
481 if searchFunc == nil {
482 return nil, fmt.Errorf("cannot search for local name %q within %s (%s.%s).%s; need a function",
483 spec.searchFor, objectKind(obj), info.Pkg.Path(), tName.Name(),
486 if isInterface(tName.Type()) {
487 return nil, fmt.Errorf("cannot search for local name %q within abstract method (%s.%s).%s",
488 spec.searchFor, info.Pkg.Path(), tName.Name(), searchFunc.Name())
492 // -- search within function or method --
494 decl := funcDecl(info, searchFunc)
496 return nil, fmt.Errorf("cannot find syntax for %s", searchFunc) // can't happen?
499 var objects []types.Object
500 for _, obj := range searchDefs(&info.Info, spec.searchFor) {
501 // We use positions, not scopes, to determine whether
502 // the obj is within searchFunc. This is clumsy, but the
503 // alternative, using the types.Scope tree, doesn't
504 // account for non-lexical objects like fields and
505 // interface methods.
506 if decl.Pos() <= obj.Pos() && obj.Pos() < decl.End() && obj != searchFunc {
507 objects = append(objects, obj)
511 return nil, fmt.Errorf("no local definition of %q within %s",
512 spec.searchFor, searchFunc)
517 func funcDecl(info *loader.PackageInfo, fn *types.Func) *ast.FuncDecl {
518 for _, f := range info.Files {
519 for _, d := range f.Decls {
520 if d, ok := d.(*ast.FuncDecl); ok && info.Defs[d.Name] == fn {
528 func searchDefs(info *types.Info, name string) []types.Object {
529 var objects []types.Object
530 for id, obj := range info.Defs {
533 // TODO(adonovan): but also implicit y in
534 // switch y := x.(type)
535 // Needs some thought.
539 objects = append(objects, obj)
545 func identAtOffset(fset *token.FileSet, f *ast.File, offset int) *ast.Ident {
547 ast.Inspect(f, func(n ast.Node) bool {
548 if id, ok := n.(*ast.Ident); ok {
549 idpos := fset.Position(id.Pos()).Offset
550 if idpos <= offset && offset < idpos+len(id.Name) {
554 return found == nil // keep traversing only until found
559 // ambiguityError returns an error describing an ambiguous "*" scope query.
560 func ambiguityError(fset *token.FileSet, objects []types.Object) error {
562 for i, obj := range objects {
564 buf.WriteString(", ")
566 posn := fset.Position(obj.Pos())
567 fmt.Fprintf(&buf, "%s at %s:%d:%d",
568 objectKind(obj), filepath.Base(posn.Filename), posn.Line, posn.Column)
570 return fmt.Errorf("ambiguous specifier %s matches %s",
571 objects[0].Name(), buf.String())
574 // Matches cgo generated comment as well as the proposed standard:
575 // https://golang.org/s/generatedcode
576 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
578 // generated reports whether ast.File is a generated file.
579 func generated(f *ast.File, tokenFile *token.File) bool {
581 // Iterate over the comments in the file
582 for _, commentGroup := range f.Comments {
583 for _, comment := range commentGroup.List {
584 if matched := generatedRx.MatchString(comment.Text); matched {
585 // Check if comment is at the beginning of the line in source
586 if pos := tokenFile.Position(comment.Slash); pos.Column == 1 {