1
2
3
4
5
6
7
8 package rename
9
10 import (
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
37 const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
38
39 Usage:
40
41 gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
42
43 You must specify the object (named entity) to rename using the -offset
44 or -from flag. Exactly one must be specified.
45
46 Flags:
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
87 gorename automatically computes the set of packages that might be
88 affected. For a local renaming, this is just the package specified by
89 -from or -offset, but for a potentially exported name, gorename scans
90 the workspace ($GOROOT and $GOPATH).
91
92 gorename rejects renamings of concrete methods that would change the
93 assignability relation between types and interfaces. If the interface
94 change was intentional, initiate the renaming at the interface method.
95
96 gorename rejects any renaming that would create a conflict at the point
97 of declaration, or a reference conflict (ambiguity or shadowing), or
98 anything else that could cause the resulting program not to compile.
99
100
101 Examples:
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 var (
139
140
141
142 Force bool
143
144
145 Diff bool
146
147
148
149 DiffCmd = "diff"
150
151
152
153 ConflictError = errors.New("renaming aborted due to conflicts")
154
155
156 Verbose bool
157 )
158
159 var stdout io.Writer = os.Stdout
160
161 type renamer struct {
162 iprog *loader.Program
163 objsToUpdate map[types.Object]bool
164 hadConflicts bool
165 from, to string
166 satisfyConstraints map[satisfy.Constraint]bool
167 packages map[*types.Package]*loader.PackageInfo
168 msets typeutil.MethodSetCache
169 changeMethods bool
170 }
171
172 var reportError = func(posn token.Position, message string) {
173 fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message)
174 }
175
176
177
178
179 func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error {
180 if fromName == to {
181 return nil
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 objsToUpdate: make(map[types.Object]bool),
202 to: to,
203 packages: map[*types.Package]*loader.PackageInfo{info.Pkg: info},
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
210 }
211 if err := r.update(); err != nil {
212 return err
213 }
214 }
215 return nil
216 }
217
218 func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error {
219
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, []byte) error) { writeFile = saved }(writeFile)
231 writeFile = diff
232 }
233
234 var spec *spec
235 var err error
236 if fromFlag != "" {
237 spec, err = parseFromFlag(ctxt, fromFlag)
238 } else {
239 spec, err = parseOffsetFlag(ctxt, offsetFlag)
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
250
251 iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true})
252 if err != nil {
253 return err
254 }
255
256 fromObjects, err := findFromObjects(iprog, spec)
257 if err != nil {
258 return err
259 }
260
261
262
263 if requiresGlobalRename(fromObjects, to) {
264
265
266
267
268
269 if Verbose {
270 log.Print("Potentially global renaming; scanning workspace...")
271 }
272
273
274 _, rev, errors := importgraph.Build(ctxt)
275 if len(errors) > 0 {
276
277
278 fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
279 for path, err := range errors {
280 fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
281 }
282 }
283
284
285 affectedPackages := make(map[string]bool)
286 for _, obj := range fromObjects {
287
288
289 for path := range rev.Search(obj.Pkg().Path()) {
290 affectedPackages[path] = true
291 }
292 }
293
294
295
296
297
298
299
300 iprog, err = loadProgram(ctxt, affectedPackages)
301 if err != nil {
302 return err
303 }
304
305 fromObjects, err = findFromObjects(iprog, spec)
306 if err != nil {
307 return err
308 }
309 }
310
311
312
313 r := renamer{
314 iprog: iprog,
315 objsToUpdate: make(map[types.Object]bool),
316 from: spec.fromName,
317 to: to,
318 packages: make(map[*types.Package]*loader.PackageInfo),
319 }
320
321
322
323
324 for _, obj := range fromObjects {
325 if obj, ok := 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
335
336
337
338
339 for _, info := range iprog.Imported {
340 r.packages[info.Pkg] = info
341 }
342 for _, info := range iprog.Created {
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
356
357
358 func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) {
359 conf := loader.Config{
360 Build: ctxt,
361 ParserMode: parser.ParseComments,
362
363
364 AllowErrors: false,
365 }
366
367
368 conf.TypeCheckFuncBodies = func(p string) bool {
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(list, pkg)
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
388
389
390
391
392 conf.AllowErrors = true
393 prog, err := conf.Load()
394 if err != nil {
395 return nil, err
396 }
397
398 var errpkgs []string
399
400 for _, info := range prog.AllPackages {
401 if containsHardErrors(info.Errors) {
402 errpkgs = append(errpkgs, info.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 nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
412 strings.Join(errpkgs, ", "), more)
413 }
414 return prog, nil
415 }
416
417 func containsHardErrors(errors []error) bool {
418 for _, err := range errors {
419 if err, ok := err.(types.Error); ok && err.Soft {
420 continue
421 }
422 return true
423 }
424 return false
425 }
426
427
428
429 func requiresGlobalRename(fromObjects []types.Object, to string) bool {
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
442
443
444
445
446
447
448 return true
449 }
450 return false
451 }
452
453
454 func (r *renamer) update() error {
455
456
457
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
464 for id, obj := 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
470 if doc := r.docComment(id); doc != nil {
471 for _, comment := range doc.List {
472 comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to)
473 }
474 }
475 }
476 }
477
478 for id, obj := 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
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(f, tokenFile) {
493 generatedFileNames = append(generatedFileNames, tokenFile.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
502 var nerrs, npkgs 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(&buf, r.iprog.Fset, f); err != nil {
519 log.Printf("failed to pretty-print syntax tree: %v", err)
520 nerrs++
521 continue
522 }
523 if err := writeFile(filename, buf.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 nidents, plural(nidents),
533 len(filesToUpdate), plural(len(filesToUpdate)),
534 npkgs, plural(npkgs))
535 }
536 if nerrs > 0 {
537 return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs))
538 }
539 return nil
540 }
541
542
543 func (r *renamer) docComment(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
554
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
571 func plural(n int) string {
572 if n != 1 {
573 return "s"
574 }
575 return ""
576 }
577
578
579 var writeFile = reallyWriteFile
580
581 func reallyWriteFile(filename string, content []byte) error {
582 return ioutil.WriteFile(filename, content, 0644)
583 }
584
585 func diff(filename string, content []byte) error {
586 renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid())
587 if err := ioutil.WriteFile(renamed, content, 0644); err != nil {
588 return err
589 }
590 defer os.Remove(renamed)
591
592 diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput()
593 if len(diff) > 0 {
594
595
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
View as plain text