golang.org/x/tools@v0.21.0/refactor/rename/rename.go (about) 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 "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 // ---- TODO ---- 112 113 // Correctness: 114 // - handle dot imports correctly 115 // - document limitations (reflection, 'implements' algorithm). 116 // - sketch a proof of exhaustiveness. 117 118 // Features: 119 // - support running on packages specified as *.go files on the command line 120 // - support running on programs containing errors (loader.Config.AllowErrors) 121 // - allow users to specify a scope other than "global" (to avoid being 122 // stuck by neglected packages in $GOPATH that don't build). 123 // - support renaming the package clause (no object) 124 // - support renaming an import path (no ident or object) 125 // (requires filesystem + SCM updates). 126 // - detect and reject edits to autogenerated files (cgo, protobufs) 127 // and optionally $GOROOT packages. 128 // - report all conflicts, or at least all qualitatively distinct ones. 129 // Sometimes we stop to avoid redundancy, but 130 // it may give a disproportionate sense of safety in -force mode. 131 // - support renaming all instances of a pattern, e.g. 132 // all receiver vars of a given type, 133 // all local variables of a given type, 134 // all PkgNames for a given package. 135 // - emit JSON output for other editors and tools. 136 137 var ( 138 // Force enables patching of the source files even if conflicts were reported. 139 // The resulting program may be ill-formed. 140 // It may even cause gorename to crash. TODO(adonovan): fix that. 141 Force bool 142 143 // Diff causes the tool to display diffs instead of rewriting files. 144 Diff bool 145 146 // DiffCmd specifies the diff command used by the -d feature. 147 // (The command must accept a -u flag and two filename arguments.) 148 DiffCmd = "diff" 149 150 // ConflictError is returned by Main when it aborts the renaming due to conflicts. 151 // (It is distinguished because the interesting errors are the conflicts themselves.) 152 ConflictError = errors.New("renaming aborted due to conflicts") 153 154 // Verbose enables extra logging. 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 // subset of iprog.AllPackages to inspect 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 // importName renames imports of fromPath within the package specified by info. 176 // If fromName is not empty, importName renames only imports as fromName. 177 // If the renaming would lead to a conflict, the file is left unchanged. 178 func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error { 179 if fromName == to { 180 return nil // no-op (e.g. rename x/foo to y/foo) 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 // ignore errors; leave the existing name 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 // -- Parse the -from or -offset specifier ---------------------------- 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 // -- Load the program consisting of the initial package ------------- 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 // -- Load a larger program, for global renamings --------------------- 261 262 if requiresGlobalRename(fromObjects, to) { 263 // For a local refactoring, we needn't load more 264 // packages, but if the renaming affects the package's 265 // API, we we must load all packages that depend on the 266 // package defining the object, plus their tests. 267 268 if Verbose { 269 log.Print("Potentially global renaming; scanning workspace...") 270 } 271 272 // Scan the workspace and build the import graph. 273 _, rev, errors := importgraph.Build(ctxt) 274 if len(errors) > 0 { 275 // With a large GOPATH tree, errors are inevitable. 276 // Report them but proceed. 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 // Enumerate the set of potentially affected packages. 284 affectedPackages := make(map[string]bool) 285 for _, obj := range fromObjects { 286 // External test packages are never imported, 287 // so they will never appear in the graph. 288 for path := range rev.Search(obj.Pkg().Path()) { 289 affectedPackages[path] = true 290 } 291 } 292 293 // TODO(adonovan): allow the user to specify the scope, 294 // or -ignore patterns? Computing the scope when we 295 // don't (yet) support inputs containing errors can make 296 // the tool rather brittle. 297 298 // Re-load the larger program. 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 // -- Do the renaming ------------------------------------------------- 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 // A renaming initiated at an interface method indicates the 321 // intention to rename abstract and concrete methods as needed 322 // to preserve assignability. 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 // Only the initially imported packages (iprog.Imported) and 334 // their external tests (iprog.Created) should be inspected or 335 // modified, as only they have type-checked functions bodies. 336 // The rest are just dependencies, needed only for package-level 337 // type information. 338 for _, info := range iprog.Imported { 339 r.packages[info.Pkg] = info 340 } 341 for _, info := range iprog.Created { // (tests) 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 // loadProgram loads the specified set of packages (plus their tests) 355 // and all their dependencies, from source, through the specified build 356 // context. Only packages in pkgs will have their functions bodies typechecked. 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 // TODO(adonovan): enable this. Requires making a lot of code more robust! 363 AllowErrors: false, 364 } 365 // Optimization: don't type-check the bodies of functions in our 366 // dependencies, since we only need exported package members. 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 // Ideally we would just return conf.Load() here, but go/types 387 // reports certain "soft" errors that gc does not (Go issue 14596). 388 // As a workaround, we set AllowErrors=true and then duplicate 389 // the loader's error checking but allow soft errors. 390 // It would be nice if the loader API permitted "AllowErrors: soft". 391 conf.AllowErrors = true 392 prog, err := conf.Load() 393 if err != nil { 394 return nil, err 395 } 396 397 var errpkgs []string 398 // Report hard errors in indirectly imported packages. 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 // requiresGlobalRename reports whether this renaming could potentially 427 // affect other packages in the Go workspace. 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 // A global renaming may be necessary even if we're 441 // exporting a previous unexported name, since if it's 442 // the name of a type, field or method, this could 443 // change selections in other packages. 444 // (We include "type" in this list because a type 445 // used as an embedded struct field entails a field 446 // renaming.) 447 return true 448 } 449 return false 450 } 451 452 // update updates the input files. 453 func (r *renamer) update() error { 454 // We use token.File, not filename, since a file may appear to 455 // belong to multiple packages and be parsed more than once. 456 // token.File captures this distinction; filename does not. 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 // Mutate the ASTs and note the filenames. 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 // Perform the rename in doc comments too. 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 // Renaming not supported if cgo files are affected. 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 // Write affected files. 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 // docComment returns the doc for an identifier. 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 // For {Type,Value}Spec, if the doc on the spec is absent, 553 // search for the enclosing GenDecl 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 // writeFile is a seam for testing and for the -d flag. 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 // diff exits with a non-zero status when the files don't match. 594 // Ignore that failure as long as we get output. 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 }