GoPLS Viewer

Home|gopls/refactor/rename/rename.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
5// Package rename contains the implementation of the 'gorename' command
6// whose main function is in golang.org/x/tools/cmd/gorename.
7// See the Usage constant for the command documentation.
8package rename // import "golang.org/x/tools/refactor/rename"
9
10import (
11    "bytes"
12    "errors"
13    "fmt"
14    "go/ast"
15    "go/build"
16    "go/format"
17    "go/parser"
18    "go/token"
19    "go/types"
20    exec "golang.org/x/sys/execabs"
21    "io"
22    "io/ioutil"
23    "log"
24    "os"
25    "path"
26    "regexp"
27    "sort"
28    "strconv"
29    "strings"
30
31    "golang.org/x/tools/go/loader"
32    "golang.org/x/tools/go/types/typeutil"
33    "golang.org/x/tools/refactor/importgraph"
34    "golang.org/x/tools/refactor/satisfy"
35)
36
37const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
38
39Usage:
40
41 gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
42
43You must specify the object (named entity) to rename using the -offset
44or -from flag.  Exactly one must be specified.
45
46Flags:
47
48-offset    specifies the filename and byte offset of an identifier to rename.
49           This form is intended for use by text editors.
50
51-from      specifies the object to rename using a query notation;
52           This form is intended for interactive use at the command line.
53           A legal -from query has one of the following forms:
54
55  "encoding/json".Decoder.Decode        method of package-level named type
56  (*"encoding/json".Decoder).Decode     ditto, alternative syntax
57  "encoding/json".Decoder.buf           field of package-level named struct type
58  "encoding/json".HTMLEscape            package member (const, func, var, type)
59  "encoding/json".Decoder.Decode::x     local object x within a method
60  "encoding/json".HTMLEscape::x         local object x within a function
61  "encoding/json"::x                    object x anywhere within a package
62  json.go::x                            object x within file json.go
63
64           Double-quotes must be escaped when writing a shell command.
65           Quotes may be omitted for single-segment import paths such as "fmt".
66
67           For methods, the parens and '*' on the receiver type are both
68           optional.
69
70           It is an error if one of the ::x queries matches multiple
71           objects.
72
73-to        the new name.
74
75-force     causes the renaming to proceed even if conflicts were reported.
76           The resulting program may be ill-formed, or experience a change
77           in behaviour.
78
79           WARNING: this flag may even cause the renaming tool to crash.
80           (In due course this bug will be fixed by moving certain
81           analyses into the type-checker.)
82
83-d         display diffs instead of rewriting files
84
85-v         enables verbose logging.
86
87gorename automatically computes the set of packages that might be
88affected.  For a local renaming, this is just the package specified by
89-from or -offset, but for a potentially exported name, gorename scans
90the workspace ($GOROOT and $GOPATH).
91
92gorename rejects renamings of concrete methods that would change the
93assignability relation between types and interfaces. If the interface
94change was intentional, initiate the renaming at the interface method.
95
96gorename rejects any renaming that would create a conflict at the point
97of declaration, or a reference conflict (ambiguity or shadowing), or
98anything else that could cause the resulting program not to compile.
99
100
101Examples:
102
103$ gorename -offset file.go:#123 -to foo
104
105  Rename the object whose identifier is at byte offset 123 within file file.go.
106
107$ gorename -from '"bytes".Buffer.Len' -to Size
108
109  Rename the "Len" method of the *bytes.Buffer type to "Size".
110`
111
112// ---- TODO ----
113
114// Correctness:
115// - handle dot imports correctly
116// - document limitations (reflection, 'implements' algorithm).
117// - sketch a proof of exhaustiveness.
118
119// Features:
120// - support running on packages specified as *.go files on the command line
121// - support running on programs containing errors (loader.Config.AllowErrors)
122// - allow users to specify a scope other than "global" (to avoid being
123//   stuck by neglected packages in $GOPATH that don't build).
124// - support renaming the package clause (no object)
125// - support renaming an import path (no ident or object)
126//   (requires filesystem + SCM updates).
127// - detect and reject edits to autogenerated files (cgo, protobufs)
128//   and optionally $GOROOT packages.
129// - report all conflicts, or at least all qualitatively distinct ones.
130//   Sometimes we stop to avoid redundancy, but
131//   it may give a disproportionate sense of safety in -force mode.
132// - support renaming all instances of a pattern, e.g.
133//   all receiver vars of a given type,
134//   all local variables of a given type,
135//   all PkgNames for a given package.
136// - emit JSON output for other editors and tools.
137
138var (
139    // Force enables patching of the source files even if conflicts were reported.
140    // The resulting program may be ill-formed.
141    // It may even cause gorename to crash.  TODO(adonovan): fix that.
142    Force bool
143
144    // Diff causes the tool to display diffs instead of rewriting files.
145    Diff bool
146
147    // DiffCmd specifies the diff command used by the -d feature.
148    // (The command must accept a -u flag and two filename arguments.)
149    DiffCmd = "diff"
150
151    // ConflictError is returned by Main when it aborts the renaming due to conflicts.
152    // (It is distinguished because the interesting errors are the conflicts themselves.)
153    ConflictError = errors.New("renaming aborted due to conflicts")
154
155    // Verbose enables extra logging.
156    Verbose bool
157)
158
159var stdout io.Writer = os.Stdout
160
161type renamer struct {
162    iprog              *loader.Program
163    objsToUpdate       map[types.Object]bool
164    hadConflicts       bool
165    fromto           string
166    satisfyConstraints map[satisfy.Constraint]bool
167    packages           map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect
168    msets              typeutil.MethodSetCache
169    changeMethods      bool
170}
171
172var reportError = func(posn token.Positionmessage string) {
173    fmt.Fprintf(os.Stderr"%s: %s\n"posnmessage)
174}
175
176// importName renames imports of fromPath within the package specified by info.
177// If fromName is not empty, importName renames only imports as fromName.
178// If the renaming would lead to a conflict, the file is left unchanged.
179func importName(iprog *loader.Programinfo *loader.PackageInfofromPathfromNameto stringerror {
180    if fromName == to {
181        return nil // no-op (e.g. rename x/foo to y/foo)
182    }
183    for _f := range info.Files {
184        var from types.Object
185        for _imp := range f.Imports {
186            importPath_ := strconv.Unquote(imp.Path.Value)
187            importName := path.Base(importPath)
188            if imp.Name != nil {
189                importName = imp.Name.Name
190            }
191            if importPath == fromPath && (fromName == "" || importName == fromName) {
192                from = info.Implicits[imp]
193                break
194            }
195        }
196        if from == nil {
197            continue
198        }
199        r := renamer{
200            iprog:        iprog,
201            objsToUpdatemake(map[types.Object]bool),
202            to:           to,
203            packages:     map[*types.Package]*loader.PackageInfo{info.Pkginfo},
204        }
205        r.check(from)
206        if r.hadConflicts {
207            reportError(iprog.Fset.Position(f.Imports[0].Pos()),
208                "skipping update of this file")
209            continue // ignore errors; leave the existing name
210        }
211        if err := r.update(); err != nil {
212            return err
213        }
214    }
215    return nil
216}
217
218func Main(ctxt *build.ContextoffsetFlagfromFlagto stringerror {
219    // -- Parse the -from or -offset specifier ----------------------------
220
221    if (offsetFlag == "") == (fromFlag == "") {
222        return fmt.Errorf("exactly one of the -from and -offset flags must be specified")
223    }
224
225    if !isValidIdentifier(to) {
226        return fmt.Errorf("-to %q: not a valid identifier"to)
227    }
228
229    if Diff {
230        defer func(saved func(string, []byteerror) { writeFile = saved }(writeFile)
231        writeFile = diff
232    }
233
234    var spec *spec
235    var err error
236    if fromFlag != "" {
237        specerr = parseFromFlag(ctxtfromFlag)
238    } else {
239        specerr = parseOffsetFlag(ctxtoffsetFlag)
240    }
241    if err != nil {
242        return err
243    }
244
245    if spec.fromName == to {
246        return fmt.Errorf("the old and new names are the same: %s"to)
247    }
248
249    // -- Load the program consisting of the initial package  -------------
250
251    iprogerr := loadProgram(ctxt, map[string]bool{spec.pkgtrue})
252    if err != nil {
253        return err
254    }
255
256    fromObjectserr := findFromObjects(iprogspec)
257    if err != nil {
258        return err
259    }
260
261    // -- Load a larger program, for global renamings ---------------------
262
263    if requiresGlobalRename(fromObjectsto) {
264        // For a local refactoring, we needn't load more
265        // packages, but if the renaming affects the package's
266        // API, we we must load all packages that depend on the
267        // package defining the object, plus their tests.
268
269        if Verbose {
270            log.Print("Potentially global renaming; scanning workspace...")
271        }
272
273        // Scan the workspace and build the import graph.
274        _reverrors := importgraph.Build(ctxt)
275        if len(errors) > 0 {
276            // With a large GOPATH tree, errors are inevitable.
277            // Report them but proceed.
278            fmt.Fprintf(os.Stderr"While scanning Go workspace:\n")
279            for patherr := range errors {
280                fmt.Fprintf(os.Stderr"Package %q: %s.\n"patherr)
281            }
282        }
283
284        // Enumerate the set of potentially affected packages.
285        affectedPackages := make(map[string]bool)
286        for _obj := range fromObjects {
287            // External test packages are never imported,
288            // so they will never appear in the graph.
289            for path := range rev.Search(obj.Pkg().Path()) {
290                affectedPackages[path] = true
291            }
292        }
293
294        // TODO(adonovan): allow the user to specify the scope,
295        // or -ignore patterns?  Computing the scope when we
296        // don't (yet) support inputs containing errors can make
297        // the tool rather brittle.
298
299        // Re-load the larger program.
300        iprogerr = loadProgram(ctxtaffectedPackages)
301        if err != nil {
302            return err
303        }
304
305        fromObjectserr = findFromObjects(iprogspec)
306        if err != nil {
307            return err
308        }
309    }
310
311    // -- Do the renaming -------------------------------------------------
312
313    r := renamer{
314        iprog:        iprog,
315        objsToUpdatemake(map[types.Object]bool),
316        from:         spec.fromName,
317        to:           to,
318        packages:     make(map[*types.Package]*loader.PackageInfo),
319    }
320
321    // A renaming initiated at an interface method indicates the
322    // intention to rename abstract and concrete methods as needed
323    // to preserve assignability.
324    for _obj := range fromObjects {
325        if objok := obj.(*types.Func); ok {
326            recv := obj.Type().(*types.Signature).Recv()
327            if recv != nil && isInterface(recv.Type().Underlying()) {
328                r.changeMethods = true
329                break
330            }
331        }
332    }
333
334    // Only the initially imported packages (iprog.Imported) and
335    // their external tests (iprog.Created) should be inspected or
336    // modified, as only they have type-checked functions bodies.
337    // The rest are just dependencies, needed only for package-level
338    // type information.
339    for _info := range iprog.Imported {
340        r.packages[info.Pkg] = info
341    }
342    for _info := range iprog.Created { // (tests)
343        r.packages[info.Pkg] = info
344    }
345
346    for _from := range fromObjects {
347        r.check(from)
348    }
349    if r.hadConflicts && !Force {
350        return ConflictError
351    }
352    return r.update()
353}
354
355// loadProgram loads the specified set of packages (plus their tests)
356// and all their dependencies, from source, through the specified build
357// context.  Only packages in pkgs will have their functions bodies typechecked.
358func loadProgram(ctxt *build.Contextpkgs map[string]bool) (*loader.Programerror) {
359    conf := loader.Config{
360        Build:      ctxt,
361        ParserModeparser.ParseComments,
362
363        // TODO(adonovan): enable this.  Requires making a lot of code more robust!
364        AllowErrorsfalse,
365    }
366    // Optimization: don't type-check the bodies of functions in our
367    // dependencies, since we only need exported package members.
368    conf.TypeCheckFuncBodies = func(p stringbool {
369        return pkgs[p] || pkgs[strings.TrimSuffix(p"_test")]
370    }
371
372    if Verbose {
373        var list []string
374        for pkg := range pkgs {
375            list = append(listpkg)
376        }
377        sort.Strings(list)
378        for _pkg := range list {
379            log.Printf("Loading package: %s"pkg)
380        }
381    }
382
383    for pkg := range pkgs {
384        conf.ImportWithTests(pkg)
385    }
386
387    // Ideally we would just return conf.Load() here, but go/types
388    // reports certain "soft" errors that gc does not (Go issue 14596).
389    // As a workaround, we set AllowErrors=true and then duplicate
390    // the loader's error checking but allow soft errors.
391    // It would be nice if the loader API permitted "AllowErrors: soft".
392    conf.AllowErrors = true
393    progerr := conf.Load()
394    if err != nil {
395        return nilerr
396    }
397
398    var errpkgs []string
399    // Report hard errors in indirectly imported packages.
400    for _info := range prog.AllPackages {
401        if containsHardErrors(info.Errors) {
402            errpkgs = append(errpkgsinfo.Pkg.Path())
403        }
404    }
405    if errpkgs != nil {
406        var more string
407        if len(errpkgs) > 3 {
408            more = fmt.Sprintf(" and %d more"len(errpkgs)-3)
409            errpkgs = errpkgs[:3]
410        }
411        return nilfmt.Errorf("couldn't load packages due to errors: %s%s",
412            strings.Join(errpkgs", "), more)
413    }
414    return prognil
415}
416
417func containsHardErrors(errors []errorbool {
418    for _err := range errors {
419        if errok := err.(types.Error); ok && err.Soft {
420            continue
421        }
422        return true
423    }
424    return false
425}
426
427// requiresGlobalRename reports whether this renaming could potentially
428// affect other packages in the Go workspace.
429func requiresGlobalRename(fromObjects []types.Objectto stringbool {
430    var tfm bool
431    for _from := range fromObjects {
432        if from.Exported() {
433            return true
434        }
435        switch objectKind(from) {
436        case "type""field""method":
437            tfm = true
438        }
439    }
440    if ast.IsExported(to) && tfm {
441        // A global renaming may be necessary even if we're
442        // exporting a previous unexported name, since if it's
443        // the name of a type, field or method, this could
444        // change selections in other packages.
445        // (We include "type" in this list because a type
446        // used as an embedded struct field entails a field
447        // renaming.)
448        return true
449    }
450    return false
451}
452
453// update updates the input files.
454func (r *renamerupdate() error {
455    // We use token.File, not filename, since a file may appear to
456    // belong to multiple packages and be parsed more than once.
457    // token.File captures this distinction; filename does not.
458
459    var nidents int
460    var filesToUpdate = make(map[*token.File]bool)
461    docRegexp := regexp.MustCompile(`\b` + r.from + `\b`)
462    for _info := range r.packages {
463        // Mutate the ASTs and note the filenames.
464        for idobj := range info.Defs {
465            if r.objsToUpdate[obj] {
466                nidents++
467                id.Name = r.to
468                filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
469                // Perform the rename in doc comments too.
470                if doc := r.docComment(id); doc != nil {
471                    for _comment := range doc.List {
472                        comment.Text = docRegexp.ReplaceAllString(comment.Textr.to)
473                    }
474                }
475            }
476        }
477
478        for idobj := range info.Uses {
479            if r.objsToUpdate[obj] {
480                nidents++
481                id.Name = r.to
482                filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
483            }
484        }
485    }
486
487    // Renaming not supported if cgo files are affected.
488    var generatedFileNames []string
489    for _info := range r.packages {
490        for _f := range info.Files {
491            tokenFile := r.iprog.Fset.File(f.Pos())
492            if filesToUpdate[tokenFile] && generated(ftokenFile) {
493                generatedFileNames = append(generatedFileNamestokenFile.Name())
494            }
495        }
496    }
497    if !Force && len(generatedFileNames) > 0 {
498        return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v"plural(len(generatedFileNames)), generatedFileNames)
499    }
500
501    // Write affected files.
502    var nerrsnpkgs int
503    for _info := range r.packages {
504        first := true
505        for _f := range info.Files {
506            tokenFile := r.iprog.Fset.File(f.Pos())
507            if filesToUpdate[tokenFile] {
508                if first {
509                    npkgs++
510                    first = false
511                    if Verbose {
512                        log.Printf("Updating package %s"info.Pkg.Path())
513                    }
514                }
515
516                filename := tokenFile.Name()
517                var buf bytes.Buffer
518                if err := format.Node(&bufr.iprog.Fsetf); err != nil {
519                    log.Printf("failed to pretty-print syntax tree: %v"err)
520                    nerrs++
521                    continue
522                }
523                if err := writeFile(filenamebuf.Bytes()); err != nil {
524                    log.Print(err)
525                    nerrs++
526                }
527            }
528        }
529    }
530    if !Diff {
531        fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n",
532            nidentsplural(nidents),
533            len(filesToUpdate), plural(len(filesToUpdate)),
534            npkgsplural(npkgs))
535    }
536    if nerrs > 0 {
537        return fmt.Errorf("failed to rewrite %d file%s"nerrsplural(nerrs))
538    }
539    return nil
540}
541
542// docComment returns the doc for an identifier.
543func (r *renamerdocComment(id *ast.Ident) *ast.CommentGroup {
544    _nodes_ := r.iprog.PathEnclosingInterval(id.Pos(), id.End())
545    for _node := range nodes {
546        switch decl := node.(type) {
547        case *ast.FuncDecl:
548            return decl.Doc
549        case *ast.Field:
550            return decl.Doc
551        case *ast.GenDecl:
552            return decl.Doc
553        // For {Type,Value}Spec, if the doc on the spec is absent,
554        // search for the enclosing GenDecl
555        case *ast.TypeSpec:
556            if decl.Doc != nil {
557                return decl.Doc
558            }
559        case *ast.ValueSpec:
560            if decl.Doc != nil {
561                return decl.Doc
562            }
563        case *ast.Ident:
564        default:
565            return nil
566        }
567    }
568    return nil
569}
570
571func plural(n intstring {
572    if n != 1 {
573        return "s"
574    }
575    return ""
576}
577
578// writeFile is a seam for testing and for the -d flag.
579var writeFile = reallyWriteFile
580
581func reallyWriteFile(filename stringcontent []byteerror {
582    return ioutil.WriteFile(filenamecontent0644)
583}
584
585func diff(filename stringcontent []byteerror {
586    renamed := fmt.Sprintf("%s.%d.renamed"filenameos.Getpid())
587    if err := ioutil.WriteFile(renamedcontent0644); err != nil {
588        return err
589    }
590    defer os.Remove(renamed)
591
592    differr := exec.Command(DiffCmd"-u"filenamerenamed).CombinedOutput()
593    if len(diff) > 0 {
594        // diff exits with a non-zero status when the files don't match.
595        // Ignore that failure as long as we get output.
596        stdout.Write(diff)
597        return nil
598    }
599    if err != nil {
600        return fmt.Errorf("computing diff: %v"err)
601    }
602    return nil
603}
604
MembersX
renamer.hadConflicts
Main.BlockStmt._
Main.r
renamer.update.RangeStmt_15117.BlockStmt.RangeStmt_15153.f
renamer.update.nerrs
renamer.update.RangeStmt_15612.BlockStmt.RangeStmt_15664.BlockStmt.BlockStmt.buf
importName.RangeStmt_6570.BlockStmt.RangeStmt_6627.BlockStmt.importPath
Main.RangeStmt_10996.info
loadProgram.prog
containsHardErrors.RangeStmt_13145.err
renamer.update.r
renamer.objsToUpdate
Main.BlockStmt.errors
Main.RangeStmt_10413.BlockStmt.BlockStmt.recv
Main.RangeStmt_11079.from
importName.RangeStmt_6570.BlockStmt.RangeStmt_6627.imp
Main.offsetFlag
Main.BlockStmt.RangeStmt_9342.BlockStmt.RangeStmt_9474.path
renamer.update.RangeStmt_15612.info
renamer.docComment._
importName.RangeStmt_6570.BlockStmt.from
loadProgram.RangeStmt_12139.pkg
DiffCmd
Main.iprog
loadProgram.BlockStmt.RangeStmt_12061.pkg
renamer.update.npkgs
diff
diff.renamed
Diff
importName.iprog
Main
requiresGlobalRename.RangeStmt_13481.from
renamer.docComment
reallyWriteFile.content
Verbose
renamer.iprog
importName.RangeStmt_6570.BlockStmt.RangeStmt_6627.BlockStmt._
renamer.update
renamer.docComment.r
renamer
Main.BlockStmt.BlockStmt.RangeStmt_9138.path
Main.RangeStmt_10413.obj
renamer.update.RangeStmt_15612.BlockStmt.RangeStmt_15664.BlockStmt.BlockStmt.filename
renamer.docComment.RangeStmt_16715.node
importName.fromName
Main.BlockStmt.BlockStmt.RangeStmt_9138.err
Main.BlockStmt.RangeStmt_9342.obj
loadProgram.RangeStmt_12686.info
renamer.update.RangeStmt_14414.info
loadProgram
loadProgram.BlockStmt.list
requiresGlobalRename
renamer.update.nidents
diff.content
Usage
renamer.changeMethods
loadProgram.pkgs
renamer.update.RangeStmt_15612.BlockStmt.RangeStmt_15664.BlockStmt.tokenFile
renamer.docComment.nodes
reallyWriteFile
renamer.docComment.id
Force
renamer.to
containsHardErrors.errors
writeFile
Main.to
diff.filename
Main.RangeStmt_10924.info
plural
importName.to
Main.spec
loadProgram.err
loadProgram.BlockStmt.more
renamer.update.docRegexp
diff.diff
typeutil
renamer.satisfyConstraints
importName.info
loadProgram.errpkgs
renamer.update.RangeStmt_15117.info
stdout
renamer.msets
importName.RangeStmt_6570.BlockStmt.err
Main.BlockStmt.affectedPackages
renamer.update.generatedFileNames
renamer.packages
Main.err
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14495.id
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14495.obj
plural.n
importName.RangeStmt_6570.BlockStmt.RangeStmt_6627.BlockStmt.importName
Main.fromFlag
containsHardErrors
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14868.obj
renamer.update.RangeStmt_15117.BlockStmt.RangeStmt_15153.BlockStmt.tokenFile
errors
Main.fromObjects
loadProgram.ctxt
reallyWriteFile.filename
Main.ctxt
requiresGlobalRename.tfm
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14495.BlockStmt.BlockStmt.doc
renamer.update.RangeStmt_15612.BlockStmt.RangeStmt_15664.f
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14868.id
sort
importName.RangeStmt_6570.f
importName.RangeStmt_6570.BlockStmt.r
loadProgram.conf
requiresGlobalRename.fromObjects
parser
importName.fromPath
loadProgram.BlockStmt.RangeStmt_11982.pkg
renamer.update.RangeStmt_15612.BlockStmt.first
Main.BlockStmt.rev
requiresGlobalRename.to
diff.err
io
renamer.from
importName
renamer.update.RangeStmt_14414.BlockStmt.RangeStmt_14495.BlockStmt.BlockStmt.BlockStmt.RangeStmt_14740.comment
renamer.update.RangeStmt_15612.BlockStmt.RangeStmt_15664.BlockStmt.BlockStmt.err
Members
X