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. |
8 | package rename // import "golang.org/x/tools/refactor/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 | // ---- 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 | |
138 | var ( |
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 | |
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 // subset of iprog.AllPackages to inspect |
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 | // 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. |
179 | func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error { |
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 | 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 // ignore errors; leave the existing name |
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 | // -- 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, []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 | // -- Load the program consisting of the initial package ------------- |
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 | // -- Load a larger program, for global renamings --------------------- |
262 | |
263 | if requiresGlobalRename(fromObjects, to) { |
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 | _, rev, errors := 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 path, err := range errors { |
280 | fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err) |
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 | 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 | // -- Do the renaming ------------------------------------------------- |
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 | // 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 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 | // 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. |
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 | // TODO(adonovan): enable this. Requires making a lot of code more robust! |
364 | AllowErrors: false, |
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 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 | // 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 | prog, err := conf.Load() |
394 | if err != nil { |
395 | return nil, err |
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(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 | // requiresGlobalRename reports whether this renaming could potentially |
428 | // affect other packages in the Go workspace. |
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 | // 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. |
454 | func (r *renamer) update() 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 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 | // 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.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 | // 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(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 | // Write affected files. |
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 | // docComment returns the doc for an identifier. |
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 | // 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 | |
571 | func plural(n int) string { |
572 | if n != 1 { |
573 | return "s" |
574 | } |
575 | return "" |
576 | } |
577 | |
578 | // writeFile is a seam for testing and for the -d flag. |
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 | // 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 |
Members