GoPLS Viewer

Home|gopls/refactor/rename/spec.go
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.
4
5package rename
6
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.
10
11import (
12    "bytes"
13    "fmt"
14    "go/ast"
15    "go/build"
16    "go/parser"
17    "go/token"
18    "go/types"
19    "log"
20    "os"
21    "path/filepath"
22    "regexp"
23    "strconv"
24    "strings"
25
26    "golang.org/x/tools/go/buildutil"
27    "golang.org/x/tools/go/loader"
28)
29
30// A spec specifies an entity to rename.
31//
32// It is populated from an -offset flag or -from query;
33// see Usage for the allowed -from query forms.
34type spec struct {
35    // pkg is the package containing the position
36    // specified by the -from or -offset flag.
37    // If filename == "", our search for the 'from' entity
38    // is restricted to this package.
39    pkg string
40
41    // The original name of the entity being renamed.
42    // If the query had a ::from component, this is that;
43    // otherwise it's the last segment, e.g.
44    //   (encoding/json.Decoder).from
45    //   encoding/json.from
46    fromName string
47
48    // -- The remaining fields are private to this file.  All are optional. --
49
50    // The query's ::x suffix, if any.
51    searchFor string
52
53    // e.g. "Decoder" in "(encoding/json.Decoder).fieldOrMethod"
54    //                or "encoding/json.Decoder
55    pkgMember string
56
57    // e.g. fieldOrMethod in "(encoding/json.Decoder).fieldOrMethod"
58    typeMember string
59
60    // Restricts the query to this file.
61    // Implied by -from="file.go::x" and -offset flags.
62    filename string
63
64    // Byte offset of the 'from' identifier within the file named 'filename'.
65    // -offset mode only.
66    offset int
67}
68
69// parseFromFlag interprets the "-from" flag value as a renaming specification.
70// See Usage in rename.go for valid formats.
71func parseFromFlag(ctxt *build.ContextfromFlag string) (*specerror) {
72    var spec spec
73    var main string // sans "::x" suffix
74    switch parts := strings.Split(fromFlag"::"); len(parts) {
75    case 1:
76        main = parts[0]
77    case 2:
78        main = parts[0]
79        spec.searchFor = parts[1]
80        if parts[1] == "" {
81            // error
82        }
83    default:
84        return nilfmt.Errorf("-from %q: invalid identifier specification (see -help for formats)"fromFlag)
85    }
86
87    if strings.HasSuffix(main".go") {
88        // main is "filename.go"
89        if spec.searchFor == "" {
90            return nilfmt.Errorf("-from: filename %q must have a ::name suffix"main)
91        }
92        spec.filename = main
93        if !buildutil.FileExists(ctxtspec.filename) {
94            return nilfmt.Errorf("no such file: %s"spec.filename)
95        }
96
97        bperr := buildutil.ContainingPackage(ctxtwdspec.filename)
98        if err != nil {
99            return nilerr
100        }
101        spec.pkg = bp.ImportPath
102
103    } else {
104        // main is one of:
105        //  "importpath"
106        //  "importpath".member
107        //  (*"importpath".type).fieldormethod           (parens and star optional)
108        if err := parseObjectSpec(&specmain); err != nil {
109            return nilerr
110        }
111    }
112
113    if spec.searchFor != "" {
114        spec.fromName = spec.searchFor
115    }
116
117    cwderr := os.Getwd()
118    if err != nil {
119        return nilerr
120    }
121
122    // Sanitize the package.
123    bperr := ctxt.Import(spec.pkgcwdbuild.FindOnly)
124    if err != nil {
125        return nilfmt.Errorf("can't find package %q"spec.pkg)
126    }
127    spec.pkg = bp.ImportPath
128
129    if !isValidIdentifier(spec.fromName) {
130        return nilfmt.Errorf("-from: invalid identifier %q"spec.fromName)
131    }
132
133    if Verbose {
134        log.Printf("-from spec: %+v"spec)
135    }
136
137    return &specnil
138}
139
140// parseObjectSpec parses main as one of the non-filename forms of
141// object specification.
142func parseObjectSpec(spec *specmain stringerror {
143    // Parse main as a Go expression, albeit a strange one.
144    e_ := parser.ParseExpr(main)
145
146    if pkg := parseImportPath(e); pkg != "" {
147        // e.g. bytes or "encoding/json": a package
148        spec.pkg = pkg
149        if spec.searchFor == "" {
150            return fmt.Errorf("-from %q: package import path %q must have a ::name suffix",
151                mainmain)
152        }
153        return nil
154    }
155
156    if eok := e.(*ast.SelectorExpr); ok {
157        x := unparen(e.X)
158
159        // Strip off star constructor, if any.
160        if starok := x.(*ast.StarExpr); ok {
161            x = star.X
162        }
163
164        if pkg := parseImportPath(x); pkg != "" {
165            // package member e.g. "encoding/json".HTMLEscape
166            spec.pkg = pkg              // e.g. "encoding/json"
167            spec.pkgMember = e.Sel.Name // e.g. "HTMLEscape"
168            spec.fromName = e.Sel.Name
169            return nil
170        }
171
172        if xok := x.(*ast.SelectorExpr); ok {
173            // field/method of type e.g. ("encoding/json".Decoder).Decode
174            y := unparen(x.X)
175            if pkg := parseImportPath(y); pkg != "" {
176                spec.pkg = pkg               // e.g. "encoding/json"
177                spec.pkgMember = x.Sel.Name  // e.g. "Decoder"
178                spec.typeMember = e.Sel.Name // e.g. "Decode"
179                spec.fromName = e.Sel.Name
180                return nil
181            }
182        }
183    }
184
185    return fmt.Errorf("-from %q: invalid expression"main)
186}
187
188// parseImportPath returns the import path of the package denoted by e.
189// Any import path may be represented as a string literal;
190// single-segment import paths (e.g. "bytes") may also be represented as
191// ast.Ident.  parseImportPath returns "" for all other expressions.
192func parseImportPath(e ast.Exprstring {
193    switch e := e.(type) {
194    case *ast.Ident:
195        return e.Name // e.g. bytes
196
197    case *ast.BasicLit:
198        if e.Kind == token.STRING {
199            pkgname_ := strconv.Unquote(e.Value)
200            return pkgname // e.g. "encoding/json"
201        }
202    }
203    return ""
204}
205
206// parseOffsetFlag interprets the "-offset" flag value as a renaming specification.
207func parseOffsetFlag(ctxt *build.ContextoffsetFlag string) (*specerror) {
208    var spec spec
209    // Validate -offset, e.g. file.go:#123
210    parts := strings.Split(offsetFlag":#")
211    if len(parts) != 2 {
212        return nilfmt.Errorf("-offset %q: invalid offset specification"offsetFlag)
213    }
214
215    spec.filename = parts[0]
216    if !buildutil.FileExists(ctxtspec.filename) {
217        return nilfmt.Errorf("no such file: %s"spec.filename)
218    }
219
220    bperr := buildutil.ContainingPackage(ctxtwdspec.filename)
221    if err != nil {
222        return nilerr
223    }
224    spec.pkg = bp.ImportPath
225
226    for _r := range parts[1] {
227        if !isDigit(r) {
228            return nilfmt.Errorf("-offset %q: non-numeric offset"offsetFlag)
229        }
230    }
231    spec.offseterr = strconv.Atoi(parts[1])
232    if err != nil {
233        return nilfmt.Errorf("-offset %q: non-numeric offset"offsetFlag)
234    }
235
236    // Parse the file and check there's an identifier at that offset.
237    fset := token.NewFileSet()
238    ferr := buildutil.ParseFile(fsetctxtnilwdspec.filenameparser.ParseComments)
239    if err != nil {
240        return nilfmt.Errorf("-offset %q: cannot parse file: %s"offsetFlagerr)
241    }
242
243    id := identAtOffset(fsetfspec.offset)
244    if id == nil {
245        return nilfmt.Errorf("-offset %q: no identifier at this position"offsetFlag)
246    }
247
248    spec.fromName = id.Name
249
250    return &specnil
251}
252
253var wd = func() string {
254    wderr := os.Getwd()
255    if err != nil {
256        panic("cannot get working directory: " + err.Error())
257    }
258    return wd
259}()
260
261// For source trees built with 'go build', the -from or -offset
262// spec identifies exactly one initial 'from' object to rename ,
263// but certain proprietary build systems allow a single file to
264// appear in multiple packages (e.g. the test package contains a
265// copy of its library), so there may be multiple objects for
266// the same source entity.
267
268func findFromObjects(iprog *loader.Programspec *spec) ([]types.Objecterror) {
269    if spec.filename != "" {
270        return findFromObjectsInFile(iprogspec)
271    }
272
273    // Search for objects defined in specified package.
274
275    // TODO(adonovan): the iprog.ImportMap has an entry {"main": ...}
276    // for main packages, even though that's not an import path.
277    // Seems like a bug.
278    //
279    // pkg := iprog.ImportMap[spec.pkg]
280    // if pkg == nil {
281    //     return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen?
282    // }
283    // info := iprog.AllPackages[pkg]
284
285    // Workaround: lookup by value.
286    var info *loader.PackageInfo
287    var pkg *types.Package
288    for pkginfo = range iprog.AllPackages {
289        if pkg.Path() == spec.pkg {
290            break
291        }
292    }
293    if info == nil {
294        return nilfmt.Errorf("package %q was not loaded"spec.pkg)
295    }
296
297    objectserr := findObjects(infospec)
298    if err != nil {
299        return nilerr
300    }
301    if len(objects) > 1 {
302        // ambiguous "*" scope query
303        return nilambiguityError(iprog.Fsetobjects)
304    }
305    return objectsnil
306}
307
308func findFromObjectsInFile(iprog *loader.Programspec *spec) ([]types.Objecterror) {
309    var fromObjects []types.Object
310    for _info := range iprog.AllPackages {
311        // restrict to specified filename
312        // NB: under certain proprietary build systems, a given
313        // filename may appear in multiple packages.
314        for _f := range info.Files {
315            thisFile := iprog.Fset.File(f.Pos())
316            if !sameFile(thisFile.Name(), spec.filename) {
317                continue
318            }
319            // This package contains the query file.
320
321            if spec.offset != 0 {
322                // We cannot refactor generated files since position information is invalidated.
323                if generated(fthisFile) {
324                    return nilfmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s"thisFile.Name())
325                }
326
327                // Search for a specific ident by file/offset.
328                id := identAtOffset(iprog.Fsetfspec.offset)
329                if id == nil {
330                    // can't happen?
331                    return nilfmt.Errorf("identifier not found")
332                }
333                obj := info.Uses[id]
334                if obj == nil {
335                    obj = info.Defs[id]
336                    if obj == nil {
337                        // Ident without Object.
338
339                        // Package clause?
340                        pos := thisFile.Pos(spec.offset)
341                        _path_ := iprog.PathEnclosingInterval(pospos)
342                        if len(path) == 2 { // [Ident File]
343                            // TODO(adonovan): support this case.
344                            return nilfmt.Errorf("cannot rename %q: renaming package clauses is not yet supported",
345                                path[1].(*ast.File).Name.Name)
346                        }
347
348                        // Implicit y in "switch y := x.(type) {"?
349                        if obj := typeSwitchVar(&info.Infopath); obj != nil {
350                            return []types.Object{obj}, nil
351                        }
352
353                        // Probably a type error.
354                        return nilfmt.Errorf("cannot find object for %q"id.Name)
355                    }
356                }
357                if obj.Pkg() == nil {
358                    return nilfmt.Errorf("cannot rename predeclared identifiers (%s)"obj)
359
360                }
361
362                fromObjects = append(fromObjectsobj)
363            } else {
364                // do a package-wide query
365                objectserr := findObjects(infospec)
366                if err != nil {
367                    return nilerr
368                }
369
370                // filter results: only objects defined in thisFile
371                var filtered []types.Object
372                for _obj := range objects {
373                    if iprog.Fset.File(obj.Pos()) == thisFile {
374                        filtered = append(filteredobj)
375                    }
376                }
377                if len(filtered) == 0 {
378                    return nilfmt.Errorf("no object %q declared in file %s",
379                        spec.fromNamespec.filename)
380                } else if len(filtered) > 1 {
381                    return nilambiguityError(iprog.Fsetfiltered)
382                }
383                fromObjects = append(fromObjectsfiltered[0])
384            }
385            break
386        }
387    }
388    if len(fromObjects) == 0 {
389        // can't happen?
390        return nilfmt.Errorf("file %s was not part of the loaded program"spec.filename)
391    }
392    return fromObjectsnil
393}
394
395func typeSwitchVar(info *types.Infopath []ast.Nodetypes.Object {
396    if len(path) > 3 {
397        // [Ident AssignStmt TypeSwitchStmt...]
398        if swok := path[2].(*ast.TypeSwitchStmt); ok {
399            // choose the first case.
400            if len(sw.Body.List) > 0 {
401                obj := info.Implicits[sw.Body.List[0].(*ast.CaseClause)]
402                if obj != nil {
403                    return obj
404                }
405            }
406        }
407    }
408    return nil
409}
410
411// On success, findObjects returns the list of objects named
412// spec.fromName matching the spec.  On success, the result has exactly
413// one element unless spec.searchFor!="", in which case it has at least one
414// element.
415func findObjects(info *loader.PackageInfospec *spec) ([]types.Objecterror) {
416    if spec.pkgMember == "" {
417        if spec.searchFor == "" {
418            panic(spec)
419        }
420        objects := searchDefs(&info.Infospec.searchFor)
421        if objects == nil {
422            return nilfmt.Errorf("no object %q declared in package %q",
423                spec.searchForinfo.Pkg.Path())
424        }
425        return objectsnil
426    }
427
428    pkgMember := info.Pkg.Scope().Lookup(spec.pkgMember)
429    if pkgMember == nil {
430        return nilfmt.Errorf("package %q has no member %q",
431            info.Pkg.Path(), spec.pkgMember)
432    }
433
434    var searchFunc *types.Func
435    if spec.typeMember == "" {
436        // package member
437        if spec.searchFor == "" {
438            return []types.Object{pkgMember}, nil
439        }
440
441        // Search within pkgMember, which must be a function.
442        searchFunc_ = pkgMember.(*types.Func)
443        if searchFunc == nil {
444            return nilfmt.Errorf("cannot search for %q within %s %q",
445                spec.searchForobjectKind(pkgMember), pkgMember)
446        }
447    } else {
448        // field/method of type
449        // e.g. (encoding/json.Decoder).Decode
450        // or ::x within it.
451
452        tName_ := pkgMember.(*types.TypeName)
453        if tName == nil {
454            return nilfmt.Errorf("%s.%s is a %s, not a type",
455                info.Pkg.Path(), pkgMember.Name(), objectKind(pkgMember))
456        }
457
458        // search within named type.
459        obj__ := types.LookupFieldOrMethod(tName.Type(), trueinfo.Pkgspec.typeMember)
460        if obj == nil {
461            return nilfmt.Errorf("cannot find field or method %q of %s %s.%s",
462                spec.typeMembertypeKind(tName.Type()), info.Pkg.Path(), tName.Name())
463        }
464
465        if spec.searchFor == "" {
466            // If it is an embedded field, return the type of the field.
467            if vok := obj.(*types.Var); ok && v.Anonymous() {
468                switch t := v.Type().(type) {
469                case *types.Pointer:
470                    return []types.Object{t.Elem().(*types.Named).Obj()}, nil
471                case *types.Named:
472                    return []types.Object{t.Obj()}, nil
473                }
474            }
475            return []types.Object{obj}, nil
476        }
477
478        searchFunc_ = obj.(*types.Func)
479        if searchFunc == nil {
480            return nilfmt.Errorf("cannot search for local name %q within %s (%s.%s).%s; need a function",
481                spec.searchForobjectKind(obj), info.Pkg.Path(), tName.Name(),
482                obj.Name())
483        }
484        if isInterface(tName.Type()) {
485            return nilfmt.Errorf("cannot search for local name %q within abstract method (%s.%s).%s",
486                spec.searchForinfo.Pkg.Path(), tName.Name(), searchFunc.Name())
487        }
488    }
489
490    // -- search within function or method --
491
492    decl := funcDecl(infosearchFunc)
493    if decl == nil {
494        return nilfmt.Errorf("cannot find syntax for %s"searchFunc// can't happen?
495    }
496
497    var objects []types.Object
498    for _obj := range searchDefs(&info.Infospec.searchFor) {
499        // We use positions, not scopes, to determine whether
500        // the obj is within searchFunc.  This is clumsy, but the
501        // alternative, using the types.Scope tree, doesn't
502        // account for non-lexical objects like fields and
503        // interface methods.
504        if decl.Pos() <= obj.Pos() && obj.Pos() < decl.End() && obj != searchFunc {
505            objects = append(objectsobj)
506        }
507    }
508    if objects == nil {
509        return nilfmt.Errorf("no local definition of %q within %s",
510            spec.searchForsearchFunc)
511    }
512    return objectsnil
513}
514
515func funcDecl(info *loader.PackageInfofn *types.Func) *ast.FuncDecl {
516    for _f := range info.Files {
517        for _d := range f.Decls {
518            if dok := d.(*ast.FuncDecl); ok && info.Defs[d.Name] == fn {
519                return d
520            }
521        }
522    }
523    return nil
524}
525
526func searchDefs(info *types.Infoname string) []types.Object {
527    var objects []types.Object
528    for idobj := range info.Defs {
529        if obj == nil {
530            // e.g. blank ident.
531            // TODO(adonovan): but also implicit y in
532            //    switch y := x.(type)
533            // Needs some thought.
534            continue
535        }
536        if id.Name == name {
537            objects = append(objectsobj)
538        }
539    }
540    return objects
541}
542
543func identAtOffset(fset *token.FileSetf *ast.Fileoffset int) *ast.Ident {
544    var found *ast.Ident
545    ast.Inspect(f, func(n ast.Nodebool {
546        if idok := n.(*ast.Ident); ok {
547            idpos := fset.Position(id.Pos()).Offset
548            if idpos <= offset && offset < idpos+len(id.Name) {
549                found = id
550            }
551        }
552        return found == nil // keep traversing only until found
553    })
554    return found
555}
556
557// ambiguityError returns an error describing an ambiguous "*" scope query.
558func ambiguityError(fset *token.FileSetobjects []types.Objecterror {
559    var buf bytes.Buffer
560    for iobj := range objects {
561        if i > 0 {
562            buf.WriteString(", ")
563        }
564        posn := fset.Position(obj.Pos())
565        fmt.Fprintf(&buf"%s at %s:%d:%d",
566            objectKind(obj), filepath.Base(posn.Filename), posn.Lineposn.Column)
567    }
568    return fmt.Errorf("ambiguous specifier %s matches %s",
569        objects[0].Name(), buf.String())
570}
571
572// Matches cgo generated comment as well as the proposed standard:
573//
574//    https://golang.org/s/generatedcode
575var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
576
577// generated reports whether ast.File is a generated file.
578func generated(f *ast.FiletokenFile *token.Filebool {
579
580    // Iterate over the comments in the file
581    for _commentGroup := range f.Comments {
582        for _comment := range commentGroup.List {
583            if matched := generatedRx.MatchString(comment.Text); matched {
584                // Check if comment is at the beginning of the line in source
585                if pos := tokenFile.Position(comment.Slash); pos.Column == 1 {
586                    return true
587                }
588            }
589        }
590    }
591    return false
592}
593
MembersX
parseObjectSpec.spec
parseOffsetFlag.err
findObjects.decl
parseOffsetFlag.id
spec.fromName
parseObjectSpec._
parseImportPath
parseOffsetFlag.fset
parseFromFlag.parts
parseOffsetFlag.ctxt
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.thisFile
findObjects.objects
searchDefs.info
generated.tokenFile
parseFromFlag.bp
findFromObjectsInFile.iprog
funcDecl.RangeStmt_14723.f
parseFromFlag.BlockStmt.err
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.err
funcDecl.RangeStmt_14723.BlockStmt.RangeStmt_14756.d
searchDefs.objects
findFromObjects.info
findFromObjectsInFile
typeSwitchVar
generated.f
generated.RangeStmt_16453.BlockStmt.RangeStmt_16497.BlockStmt.matched
ambiguityError.fset
generated.RangeStmt_16453.BlockStmt.RangeStmt_16497.comment
spec.offset
parseOffsetFlag.bp
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.BlockStmt.BlockStmt._
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.filtered
ambiguityError.buf
findFromObjectsInFile.RangeStmt_8368.info
parseFromFlag.cwd
parseObjectSpec.pkg
spec.pkg
parseFromFlag.ctxt
BlockStmt.err
findFromObjects.RangeStmt_7863.pkg
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.f
findObjects.info
spec.typeMember
parseObjectSpec.main
parseOffsetFlag.RangeStmt_6007.r
typeSwitchVar.info
findObjects.BlockStmt._
parseObjectSpec.BlockStmt.pkg
findObjects.pkgMember
ambiguityError.objects
parseFromFlag.main
parseObjectSpec.BlockStmt.BlockStmt.y
findFromObjects.RangeStmt_7863.info
findFromObjects.objects
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.BlockStmt.BlockStmt.pos
findObjects.searchFunc
generated.RangeStmt_16453.commentGroup
parseOffsetFlag
ambiguityError.RangeStmt_15807.i
parseObjectSpec.BlockStmt.x
parseOffsetFlag.spec
parseOffsetFlag.parts
funcDecl.info
searchDefs.RangeStmt_14983.obj
ambiguityError.RangeStmt_15807.obj
findFromObjects.spec
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.id
findObjects.BlockStmt.objects
parseObjectSpec.e
findFromObjects
findFromObjectsInFile.spec
findFromObjectsInFile.fromObjects
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.BlockStmt.BlockStmt.path
findObjects
findObjects.BlockStmt.obj
findObjects.RangeStmt_14080.obj
identAtOffset
identAtOffset.offset
spec.searchFor
parseFromFlag.err
parseObjectSpec
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.RangeStmt_10344.obj
findObjects.spec
parseFromFlag.spec
identAtOffset.f
generated.RangeStmt_16453.BlockStmt.RangeStmt_16497.BlockStmt.BlockStmt.pos
spec.pkgMember
parseOffsetFlag.f
findFromObjects.iprog
identAtOffset.fset
identAtOffset.BlockStmt.BlockStmt.idpos
spec.filename
parseImportPath.BlockStmt.BlockStmt._
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.BlockStmt.BlockStmt.obj
typeSwitchVar.path
searchDefs
identAtOffset.found
ambiguityError
spec
parseFromFlag.fromFlag
parseImportPath.BlockStmt.BlockStmt.pkgname
parseOffsetFlag.offsetFlag
BlockStmt.wd
funcDecl.fn
ambiguityError.RangeStmt_15807.BlockStmt.posn
parseObjectSpec.BlockStmt.BlockStmt.pkg
searchDefs.RangeStmt_14983.id
parseFromFlag.BlockStmt.bp
parseImportPath.e
parseFromFlag
findFromObjects.pkg
findFromObjects.err
findFromObjectsInFile.RangeStmt_8368.BlockStmt.RangeStmt_8552.BlockStmt.BlockStmt.objects
funcDecl
searchDefs.name
generated
Members
X