github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/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 "github.com/jhump/golang-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 "github.com/jhump/golang-x-tools/go/loader" 32 "github.com/jhump/golang-x-tools/go/types/typeutil" 33 "github.com/jhump/golang-x-tools/refactor/importgraph" 34 "github.com/jhump/golang-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 }