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 "io"
21 "log"
22 "os"
23 "os/exec"
24 "path"
25 "regexp"
26 "sort"
27 "strconv"
28 "strings"
29
30 "golang.org/x/tools/go/loader"
31 "golang.org/x/tools/go/types/typeutil"
32 "golang.org/x/tools/refactor/importgraph"
33 "golang.org/x/tools/refactor/satisfy"
34 )
35
36 const Usage = `gorename: precise type-safe renaming of identifiers in Go source code.
37
38 Usage:
39
40 gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force]
41
42 You must specify the object (named entity) to rename using the -offset
43 or -from flag. Exactly one must be specified.
44
45 Flags:
46
47 -offset specifies the filename and byte offset of an identifier to rename.
48 This form is intended for use by text editors.
49
50 -from specifies the object to rename using a query notation;
51 This form is intended for interactive use at the command line.
52 A legal -from query has one of the following forms:
53
54 "encoding/json".Decoder.Decode method of package-level named type
55 (*"encoding/json".Decoder).Decode ditto, alternative syntax
56 "encoding/json".Decoder.buf field of package-level named struct type
57 "encoding/json".HTMLEscape package member (const, func, var, type)
58 "encoding/json".Decoder.Decode::x local object x within a method
59 "encoding/json".HTMLEscape::x local object x within a function
60 "encoding/json"::x object x anywhere within a package
61 json.go::x object x within file json.go
62
63 Double-quotes must be escaped when writing a shell command.
64 Quotes may be omitted for single-segment import paths such as "fmt".
65
66 For methods, the parens and '*' on the receiver type are both
67 optional.
68
69 It is an error if one of the ::x queries matches multiple
70 objects.
71
72 -to the new name.
73
74 -force causes the renaming to proceed even if conflicts were reported.
75 The resulting program may be ill-formed, or experience a change
76 in behaviour.
77
78 WARNING: this flag may even cause the renaming tool to crash.
79 (In due course this bug will be fixed by moving certain
80 analyses into the type-checker.)
81
82 -d display diffs instead of rewriting files
83
84 -v enables verbose logging.
85
86 gorename automatically computes the set of packages that might be
87 affected. For a local renaming, this is just the package specified by
88 -from or -offset, but for a potentially exported name, gorename scans
89 the workspace ($GOROOT and $GOPATH).
90
91 gorename rejects renamings of concrete methods that would change the
92 assignability relation between types and interfaces. If the interface
93 change was intentional, initiate the renaming at the interface method.
94
95 gorename rejects any renaming that would create a conflict at the point
96 of declaration, or a reference conflict (ambiguity or shadowing), or
97 anything else that could cause the resulting program not to compile.
98
99
100 Examples:
101
102 $ gorename -offset file.go:#123 -to foo
103
104 Rename the object whose identifier is at byte offset 123 within file file.go.
105
106 $ gorename -from '"bytes".Buffer.Len' -to Size
107
108 Rename the "Len" method of the *bytes.Buffer type to "Size".
109 `
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 var (
138
139
140
141 Force bool
142
143
144 Diff bool
145
146
147
148 DiffCmd = "diff"
149
150
151
152 ConflictError = errors.New("renaming aborted due to conflicts")
153
154
155 Verbose bool
156 )
157
158 var stdout io.Writer = os.Stdout
159
160 type renamer struct {
161 iprog *loader.Program
162 objsToUpdate map[types.Object]bool
163 hadConflicts bool
164 from, to string
165 satisfyConstraints map[satisfy.Constraint]bool
166 packages map[*types.Package]*loader.PackageInfo
167 msets typeutil.MethodSetCache
168 changeMethods bool
169 }
170
171 var reportError = func(posn token.Position, message string) {
172 fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message)
173 }
174
175
176
177
178 func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error {
179 if fromName == to {
180 return nil
181 }
182 for _, f := range info.Files {
183 var from types.Object
184 for _, imp := range f.Imports {
185 importPath, _ := strconv.Unquote(imp.Path.Value)
186 importName := path.Base(importPath)
187 if imp.Name != nil {
188 importName = imp.Name.Name
189 }
190 if importPath == fromPath && (fromName == "" || importName == fromName) {
191 from = info.Implicits[imp]
192 break
193 }
194 }
195 if from == nil {
196 continue
197 }
198 r := renamer{
199 iprog: iprog,
200 objsToUpdate: make(map[types.Object]bool),
201 to: to,
202 packages: map[*types.Package]*loader.PackageInfo{info.Pkg: info},
203 }
204 r.check(from)
205 if r.hadConflicts {
206 reportError(iprog.Fset.Position(f.Imports[0].Pos()),
207 "skipping update of this file")
208 continue
209 }
210 if err := r.update(); err != nil {
211 return err
212 }
213 }
214 return nil
215 }
216
217 func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error {
218
219
220 if (offsetFlag == "") == (fromFlag == "") {
221 return fmt.Errorf("exactly one of the -from and -offset flags must be specified")
222 }
223
224 if !isValidIdentifier(to) {
225 return fmt.Errorf("-to %q: not a valid identifier", to)
226 }
227
228 if Diff {
229 defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile)
230 writeFile = diff
231 }
232
233 var spec *spec
234 var err error
235 if fromFlag != "" {
236 spec, err = parseFromFlag(ctxt, fromFlag)
237 } else {
238 spec, err = parseOffsetFlag(ctxt, offsetFlag)
239 }
240 if err != nil {
241 return err
242 }
243
244 if spec.fromName == to {
245 return fmt.Errorf("the old and new names are the same: %s", to)
246 }
247
248
249
250 iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true})
251 if err != nil {
252 return err
253 }
254
255 fromObjects, err := findFromObjects(iprog, spec)
256 if err != nil {
257 return err
258 }
259
260
261
262 if requiresGlobalRename(fromObjects, to) {
263
264
265
266
267
268 if Verbose {
269 log.Print("Potentially global renaming; scanning workspace...")
270 }
271
272
273 _, rev, errors := importgraph.Build(ctxt)
274 if len(errors) > 0 {
275
276
277 fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
278 for path, err := range errors {
279 fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
280 }
281 }
282
283
284 affectedPackages := make(map[string]bool)
285 for _, obj := range fromObjects {
286
287
288 for path := range rev.Search(obj.Pkg().Path()) {
289 affectedPackages[path] = true
290 }
291 }
292
293
294
295
296
297
298
299 iprog, err = loadProgram(ctxt, affectedPackages)
300 if err != nil {
301 return err
302 }
303
304 fromObjects, err = findFromObjects(iprog, spec)
305 if err != nil {
306 return err
307 }
308 }
309
310
311
312 r := renamer{
313 iprog: iprog,
314 objsToUpdate: make(map[types.Object]bool),
315 from: spec.fromName,
316 to: to,
317 packages: make(map[*types.Package]*loader.PackageInfo),
318 }
319
320
321
322
323 for _, obj := range fromObjects {
324 if obj, ok := obj.(*types.Func); ok {
325 recv := obj.Type().(*types.Signature).Recv()
326 if recv != nil && types.IsInterface(recv.Type()) {
327 r.changeMethods = true
328 break
329 }
330 }
331 }
332
333
334
335
336
337
338 for _, info := range iprog.Imported {
339 r.packages[info.Pkg] = info
340 }
341 for _, info := range iprog.Created {
342 r.packages[info.Pkg] = info
343 }
344
345 for _, from := range fromObjects {
346 r.check(from)
347 }
348 if r.hadConflicts && !Force {
349 return ConflictError
350 }
351 return r.update()
352 }
353
354
355
356
357 func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) {
358 conf := loader.Config{
359 Build: ctxt,
360 ParserMode: parser.ParseComments,
361
362
363 AllowErrors: false,
364 }
365
366
367 conf.TypeCheckFuncBodies = func(p string) bool {
368 return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")]
369 }
370
371 if Verbose {
372 var list []string
373 for pkg := range pkgs {
374 list = append(list, pkg)
375 }
376 sort.Strings(list)
377 for _, pkg := range list {
378 log.Printf("Loading package: %s", pkg)
379 }
380 }
381
382 for pkg := range pkgs {
383 conf.ImportWithTests(pkg)
384 }
385
386
387
388
389
390
391 conf.AllowErrors = true
392 prog, err := conf.Load()
393 if err != nil {
394 return nil, err
395 }
396
397 var errpkgs []string
398
399 for _, info := range prog.AllPackages {
400 if containsHardErrors(info.Errors) {
401 errpkgs = append(errpkgs, info.Pkg.Path())
402 }
403 }
404 if errpkgs != nil {
405 var more string
406 if len(errpkgs) > 3 {
407 more = fmt.Sprintf(" and %d more", len(errpkgs)-3)
408 errpkgs = errpkgs[:3]
409 }
410 return nil, fmt.Errorf("couldn't load packages due to errors: %s%s",
411 strings.Join(errpkgs, ", "), more)
412 }
413 return prog, nil
414 }
415
416 func containsHardErrors(errors []error) bool {
417 for _, err := range errors {
418 if err, ok := err.(types.Error); ok && err.Soft {
419 continue
420 }
421 return true
422 }
423 return false
424 }
425
426
427
428 func requiresGlobalRename(fromObjects []types.Object, to string) bool {
429 var tfm bool
430 for _, from := range fromObjects {
431 if from.Exported() {
432 return true
433 }
434 switch objectKind(from) {
435 case "type", "field", "method":
436 tfm = true
437 }
438 }
439 if ast.IsExported(to) && tfm {
440
441
442
443
444
445
446
447 return true
448 }
449 return false
450 }
451
452
453 func (r *renamer) update() error {
454
455
456
457
458 var nidents int
459 var filesToUpdate = make(map[*token.File]bool)
460 docRegexp := regexp.MustCompile(`\b` + r.from + `\b`)
461 for _, info := range r.packages {
462
463 for id, obj := range info.Defs {
464 if r.objsToUpdate[obj] {
465 nidents++
466 id.Name = r.to
467 filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
468
469 if doc := r.docComment(id); doc != nil {
470 for _, comment := range doc.List {
471 comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to)
472 }
473 }
474 }
475 }
476
477 for id, obj := range info.Uses {
478 if r.objsToUpdate[obj] {
479 nidents++
480 id.Name = r.to
481 filesToUpdate[r.iprog.Fset.File(id.Pos())] = true
482 }
483 }
484 }
485
486
487 var generatedFileNames []string
488 for _, info := range r.packages {
489 for _, f := range info.Files {
490 tokenFile := r.iprog.Fset.File(f.Pos())
491 if filesToUpdate[tokenFile] && generated(f, tokenFile) {
492 generatedFileNames = append(generatedFileNames, tokenFile.Name())
493 }
494 }
495 }
496 if !Force && len(generatedFileNames) > 0 {
497 return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames)
498 }
499
500
501 var nerrs, npkgs int
502 for _, info := range r.packages {
503 first := true
504 for _, f := range info.Files {
505 tokenFile := r.iprog.Fset.File(f.Pos())
506 if filesToUpdate[tokenFile] {
507 if first {
508 npkgs++
509 first = false
510 if Verbose {
511 log.Printf("Updating package %s", info.Pkg.Path())
512 }
513 }
514
515 filename := tokenFile.Name()
516 var buf bytes.Buffer
517 if err := format.Node(&buf, r.iprog.Fset, f); err != nil {
518 log.Printf("failed to pretty-print syntax tree: %v", err)
519 nerrs++
520 continue
521 }
522 if err := writeFile(filename, buf.Bytes()); err != nil {
523 log.Print(err)
524 nerrs++
525 }
526 }
527 }
528 }
529 if !Diff {
530 fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n",
531 nidents, plural(nidents),
532 len(filesToUpdate), plural(len(filesToUpdate)),
533 npkgs, plural(npkgs))
534 }
535 if nerrs > 0 {
536 return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs))
537 }
538 return nil
539 }
540
541
542 func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup {
543 _, nodes, _ := r.iprog.PathEnclosingInterval(id.Pos(), id.End())
544 for _, node := range nodes {
545 switch decl := node.(type) {
546 case *ast.FuncDecl:
547 return decl.Doc
548 case *ast.Field:
549 return decl.Doc
550 case *ast.GenDecl:
551 return decl.Doc
552
553
554 case *ast.TypeSpec:
555 if decl.Doc != nil {
556 return decl.Doc
557 }
558 case *ast.ValueSpec:
559 if decl.Doc != nil {
560 return decl.Doc
561 }
562 case *ast.Ident:
563 default:
564 return nil
565 }
566 }
567 return nil
568 }
569
570 func plural(n int) string {
571 if n != 1 {
572 return "s"
573 }
574 return ""
575 }
576
577
578 var writeFile = reallyWriteFile
579
580 func reallyWriteFile(filename string, content []byte) error {
581 return os.WriteFile(filename, content, 0644)
582 }
583
584 func diff(filename string, content []byte) error {
585 renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid())
586 if err := os.WriteFile(renamed, content, 0644); err != nil {
587 return err
588 }
589 defer os.Remove(renamed)
590
591 diff, err := exec.Command(DiffCmd, "-u", filename, renamed).Output()
592 if len(diff) > 0 {
593
594
595 stdout.Write(diff)
596 return nil
597 }
598 if err != nil {
599 if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 {
600 err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr)
601 }
602 return fmt.Errorf("computing diff: %v", err)
603 }
604 return nil
605 }
606
View as plain text