github.com/v2fly/tools@v0.100.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 github.com/v2fly/tools/cmd/gorename. 7 // See the Usage constant for the command documentation. 8 package rename // import "github.com/v2fly/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 "io/ioutil" 22 "log" 23 "os" 24 "path" 25 "regexp" 26 "sort" 27 "strconv" 28 "strings" 29 30 exec "golang.org/x/sys/execabs" 31 32 "github.com/v2fly/tools/go/loader" 33 "github.com/v2fly/tools/go/types/typeutil" 34 "github.com/v2fly/tools/refactor/importgraph" 35 "github.com/v2fly/tools/refactor/satisfy" 36 ) 37 38 const Usage = `gorename: precise type-safe renaming of identifiers in Go source code. 39 40 Usage: 41 42 gorename (-from <spec> | -offset <file>:#<byte-offset>) -to <name> [-force] 43 44 You must specify the object (named entity) to rename using the -offset 45 or -from flag. Exactly one must be specified. 46 47 Flags: 48 49 -offset specifies the filename and byte offset of an identifier to rename. 50 This form is intended for use by text editors. 51 52 -from specifies the object to rename using a query notation; 53 This form is intended for interactive use at the command line. 54 A legal -from query has one of the following forms: 55 56 "encoding/json".Decoder.Decode method of package-level named type 57 (*"encoding/json".Decoder).Decode ditto, alternative syntax 58 "encoding/json".Decoder.buf field of package-level named struct type 59 "encoding/json".HTMLEscape package member (const, func, var, type) 60 "encoding/json".Decoder.Decode::x local object x within a method 61 "encoding/json".HTMLEscape::x local object x within a function 62 "encoding/json"::x object x anywhere within a package 63 json.go::x object x within file json.go 64 65 Double-quotes must be escaped when writing a shell command. 66 Quotes may be omitted for single-segment import paths such as "fmt". 67 68 For methods, the parens and '*' on the receiver type are both 69 optional. 70 71 It is an error if one of the ::x queries matches multiple 72 objects. 73 74 -to the new name. 75 76 -force causes the renaming to proceed even if conflicts were reported. 77 The resulting program may be ill-formed, or experience a change 78 in behaviour. 79 80 WARNING: this flag may even cause the renaming tool to crash. 81 (In due course this bug will be fixed by moving certain 82 analyses into the type-checker.) 83 84 -d display diffs instead of rewriting files 85 86 -v enables verbose logging. 87 88 gorename automatically computes the set of packages that might be 89 affected. For a local renaming, this is just the package specified by 90 -from or -offset, but for a potentially exported name, gorename scans 91 the workspace ($GOROOT and $GOPATH). 92 93 gorename rejects renamings of concrete methods that would change the 94 assignability relation between types and interfaces. If the interface 95 change was intentional, initiate the renaming at the interface method. 96 97 gorename rejects any renaming that would create a conflict at the point 98 of declaration, or a reference conflict (ambiguity or shadowing), or 99 anything else that could cause the resulting program not to compile. 100 101 102 Examples: 103 104 $ gorename -offset file.go:#123 -to foo 105 106 Rename the object whose identifier is at byte offset 123 within file file.go. 107 108 $ gorename -from '"bytes".Buffer.Len' -to Size 109 110 Rename the "Len" method of the *bytes.Buffer type to "Size". 111 ` 112 113 // ---- TODO ---- 114 115 // Correctness: 116 // - handle dot imports correctly 117 // - document limitations (reflection, 'implements' algorithm). 118 // - sketch a proof of exhaustiveness. 119 120 // Features: 121 // - support running on packages specified as *.go files on the command line 122 // - support running on programs containing errors (loader.Config.AllowErrors) 123 // - allow users to specify a scope other than "global" (to avoid being 124 // stuck by neglected packages in $GOPATH that don't build). 125 // - support renaming the package clause (no object) 126 // - support renaming an import path (no ident or object) 127 // (requires filesystem + SCM updates). 128 // - detect and reject edits to autogenerated files (cgo, protobufs) 129 // and optionally $GOROOT packages. 130 // - report all conflicts, or at least all qualitatively distinct ones. 131 // Sometimes we stop to avoid redundancy, but 132 // it may give a disproportionate sense of safety in -force mode. 133 // - support renaming all instances of a pattern, e.g. 134 // all receiver vars of a given type, 135 // all local variables of a given type, 136 // all PkgNames for a given package. 137 // - emit JSON output for other editors and tools. 138 139 var ( 140 // Force enables patching of the source files even if conflicts were reported. 141 // The resulting program may be ill-formed. 142 // It may even cause gorename to crash. TODO(adonovan): fix that. 143 Force bool 144 145 // Diff causes the tool to display diffs instead of rewriting files. 146 Diff bool 147 148 // DiffCmd specifies the diff command used by the -d feature. 149 // (The command must accept a -u flag and two filename arguments.) 150 DiffCmd = "diff" 151 152 // ConflictError is returned by Main when it aborts the renaming due to conflicts. 153 // (It is distinguished because the interesting errors are the conflicts themselves.) 154 ConflictError = errors.New("renaming aborted due to conflicts") 155 156 // Verbose enables extra logging. 157 Verbose bool 158 ) 159 160 var stdout io.Writer = os.Stdout 161 162 type renamer struct { 163 iprog *loader.Program 164 objsToUpdate map[types.Object]bool 165 hadConflicts bool 166 from, to string 167 satisfyConstraints map[satisfy.Constraint]bool 168 packages map[*types.Package]*loader.PackageInfo // subset of iprog.AllPackages to inspect 169 msets typeutil.MethodSetCache 170 changeMethods bool 171 } 172 173 var reportError = func(posn token.Position, message string) { 174 fmt.Fprintf(os.Stderr, "%s: %s\n", posn, message) 175 } 176 177 // importName renames imports of fromPath within the package specified by info. 178 // If fromName is not empty, importName renames only imports as fromName. 179 // If the renaming would lead to a conflict, the file is left unchanged. 180 func importName(iprog *loader.Program, info *loader.PackageInfo, fromPath, fromName, to string) error { 181 if fromName == to { 182 return nil // no-op (e.g. rename x/foo to y/foo) 183 } 184 for _, f := range info.Files { 185 var from types.Object 186 for _, imp := range f.Imports { 187 importPath, _ := strconv.Unquote(imp.Path.Value) 188 importName := path.Base(importPath) 189 if imp.Name != nil { 190 importName = imp.Name.Name 191 } 192 if importPath == fromPath && (fromName == "" || importName == fromName) { 193 from = info.Implicits[imp] 194 break 195 } 196 } 197 if from == nil { 198 continue 199 } 200 r := renamer{ 201 iprog: iprog, 202 objsToUpdate: make(map[types.Object]bool), 203 to: to, 204 packages: map[*types.Package]*loader.PackageInfo{info.Pkg: info}, 205 } 206 r.check(from) 207 if r.hadConflicts { 208 reportError(iprog.Fset.Position(f.Imports[0].Pos()), 209 "skipping update of this file") 210 continue // ignore errors; leave the existing name 211 } 212 if err := r.update(); err != nil { 213 return err 214 } 215 } 216 return nil 217 } 218 219 func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error { 220 // -- Parse the -from or -offset specifier ---------------------------- 221 222 if (offsetFlag == "") == (fromFlag == "") { 223 return fmt.Errorf("exactly one of the -from and -offset flags must be specified") 224 } 225 226 if !isValidIdentifier(to) { 227 return fmt.Errorf("-to %q: not a valid identifier", to) 228 } 229 230 if Diff { 231 defer func(saved func(string, []byte) error) { writeFile = saved }(writeFile) 232 writeFile = diff 233 } 234 235 var spec *spec 236 var err error 237 if fromFlag != "" { 238 spec, err = parseFromFlag(ctxt, fromFlag) 239 } else { 240 spec, err = parseOffsetFlag(ctxt, offsetFlag) 241 } 242 if err != nil { 243 return err 244 } 245 246 if spec.fromName == to { 247 return fmt.Errorf("the old and new names are the same: %s", to) 248 } 249 250 // -- Load the program consisting of the initial package ------------- 251 252 iprog, err := loadProgram(ctxt, map[string]bool{spec.pkg: true}) 253 if err != nil { 254 return err 255 } 256 257 fromObjects, err := findFromObjects(iprog, spec) 258 if err != nil { 259 return err 260 } 261 262 // -- Load a larger program, for global renamings --------------------- 263 264 if requiresGlobalRename(fromObjects, to) { 265 // For a local refactoring, we needn't load more 266 // packages, but if the renaming affects the package's 267 // API, we we must load all packages that depend on the 268 // package defining the object, plus their tests. 269 270 if Verbose { 271 log.Print("Potentially global renaming; scanning workspace...") 272 } 273 274 // Scan the workspace and build the import graph. 275 _, rev, errors := importgraph.Build(ctxt) 276 if len(errors) > 0 { 277 // With a large GOPATH tree, errors are inevitable. 278 // Report them but proceed. 279 fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n") 280 for path, err := range errors { 281 fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err) 282 } 283 } 284 285 // Enumerate the set of potentially affected packages. 286 affectedPackages := make(map[string]bool) 287 for _, obj := range fromObjects { 288 // External test packages are never imported, 289 // so they will never appear in the graph. 290 for path := range rev.Search(obj.Pkg().Path()) { 291 affectedPackages[path] = true 292 } 293 } 294 295 // TODO(adonovan): allow the user to specify the scope, 296 // or -ignore patterns? Computing the scope when we 297 // don't (yet) support inputs containing errors can make 298 // the tool rather brittle. 299 300 // Re-load the larger program. 301 iprog, err = loadProgram(ctxt, affectedPackages) 302 if err != nil { 303 return err 304 } 305 306 fromObjects, err = findFromObjects(iprog, spec) 307 if err != nil { 308 return err 309 } 310 } 311 312 // -- Do the renaming ------------------------------------------------- 313 314 r := renamer{ 315 iprog: iprog, 316 objsToUpdate: make(map[types.Object]bool), 317 from: spec.fromName, 318 to: to, 319 packages: make(map[*types.Package]*loader.PackageInfo), 320 } 321 322 // A renaming initiated at an interface method indicates the 323 // intention to rename abstract and concrete methods as needed 324 // to preserve assignability. 325 for _, obj := range fromObjects { 326 if obj, ok := obj.(*types.Func); ok { 327 recv := obj.Type().(*types.Signature).Recv() 328 if recv != nil && isInterface(recv.Type().Underlying()) { 329 r.changeMethods = true 330 break 331 } 332 } 333 } 334 335 // Only the initially imported packages (iprog.Imported) and 336 // their external tests (iprog.Created) should be inspected or 337 // modified, as only they have type-checked functions bodies. 338 // The rest are just dependencies, needed only for package-level 339 // type information. 340 for _, info := range iprog.Imported { 341 r.packages[info.Pkg] = info 342 } 343 for _, info := range iprog.Created { // (tests) 344 r.packages[info.Pkg] = info 345 } 346 347 for _, from := range fromObjects { 348 r.check(from) 349 } 350 if r.hadConflicts && !Force { 351 return ConflictError 352 } 353 return r.update() 354 } 355 356 // loadProgram loads the specified set of packages (plus their tests) 357 // and all their dependencies, from source, through the specified build 358 // context. Only packages in pkgs will have their functions bodies typechecked. 359 func loadProgram(ctxt *build.Context, pkgs map[string]bool) (*loader.Program, error) { 360 conf := loader.Config{ 361 Build: ctxt, 362 ParserMode: parser.ParseComments, 363 364 // TODO(adonovan): enable this. Requires making a lot of code more robust! 365 AllowErrors: false, 366 } 367 // Optimization: don't type-check the bodies of functions in our 368 // dependencies, since we only need exported package members. 369 conf.TypeCheckFuncBodies = func(p string) bool { 370 return pkgs[p] || pkgs[strings.TrimSuffix(p, "_test")] 371 } 372 373 if Verbose { 374 var list []string 375 for pkg := range pkgs { 376 list = append(list, pkg) 377 } 378 sort.Strings(list) 379 for _, pkg := range list { 380 log.Printf("Loading package: %s", pkg) 381 } 382 } 383 384 for pkg := range pkgs { 385 conf.ImportWithTests(pkg) 386 } 387 388 // Ideally we would just return conf.Load() here, but go/types 389 // reports certain "soft" errors that gc does not (Go issue 14596). 390 // As a workaround, we set AllowErrors=true and then duplicate 391 // the loader's error checking but allow soft errors. 392 // It would be nice if the loader API permitted "AllowErrors: soft". 393 conf.AllowErrors = true 394 prog, err := conf.Load() 395 if err != nil { 396 return nil, err 397 } 398 399 var errpkgs []string 400 // Report hard errors in indirectly imported packages. 401 for _, info := range prog.AllPackages { 402 if containsHardErrors(info.Errors) { 403 errpkgs = append(errpkgs, info.Pkg.Path()) 404 } 405 } 406 if errpkgs != nil { 407 var more string 408 if len(errpkgs) > 3 { 409 more = fmt.Sprintf(" and %d more", len(errpkgs)-3) 410 errpkgs = errpkgs[:3] 411 } 412 return nil, fmt.Errorf("couldn't load packages due to errors: %s%s", 413 strings.Join(errpkgs, ", "), more) 414 } 415 return prog, nil 416 } 417 418 func containsHardErrors(errors []error) bool { 419 for _, err := range errors { 420 if err, ok := err.(types.Error); ok && err.Soft { 421 continue 422 } 423 return true 424 } 425 return false 426 } 427 428 // requiresGlobalRename reports whether this renaming could potentially 429 // affect other packages in the Go workspace. 430 func requiresGlobalRename(fromObjects []types.Object, to string) bool { 431 var tfm bool 432 for _, from := range fromObjects { 433 if from.Exported() { 434 return true 435 } 436 switch objectKind(from) { 437 case "type", "field", "method": 438 tfm = true 439 } 440 } 441 if ast.IsExported(to) && tfm { 442 // A global renaming may be necessary even if we're 443 // exporting a previous unexported name, since if it's 444 // the name of a type, field or method, this could 445 // change selections in other packages. 446 // (We include "type" in this list because a type 447 // used as an embedded struct field entails a field 448 // renaming.) 449 return true 450 } 451 return false 452 } 453 454 // update updates the input files. 455 func (r *renamer) update() error { 456 // We use token.File, not filename, since a file may appear to 457 // belong to multiple packages and be parsed more than once. 458 // token.File captures this distinction; filename does not. 459 460 var nidents int 461 var filesToUpdate = make(map[*token.File]bool) 462 docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) 463 for _, info := range r.packages { 464 // Mutate the ASTs and note the filenames. 465 for id, obj := range info.Defs { 466 if r.objsToUpdate[obj] { 467 nidents++ 468 id.Name = r.to 469 filesToUpdate[r.iprog.Fset.File(id.Pos())] = true 470 // Perform the rename in doc comments too. 471 if doc := r.docComment(id); doc != nil { 472 for _, comment := range doc.List { 473 comment.Text = docRegexp.ReplaceAllString(comment.Text, r.to) 474 } 475 } 476 } 477 } 478 479 for id, obj := range info.Uses { 480 if r.objsToUpdate[obj] { 481 nidents++ 482 id.Name = r.to 483 filesToUpdate[r.iprog.Fset.File(id.Pos())] = true 484 } 485 } 486 } 487 488 // Renaming not supported if cgo files are affected. 489 var generatedFileNames []string 490 for _, info := range r.packages { 491 for _, f := range info.Files { 492 tokenFile := r.iprog.Fset.File(f.Pos()) 493 if filesToUpdate[tokenFile] && generated(f, tokenFile) { 494 generatedFileNames = append(generatedFileNames, tokenFile.Name()) 495 } 496 } 497 } 498 if !Force && len(generatedFileNames) > 0 { 499 return fmt.Errorf("refusing to modify generated file%s containing DO NOT EDIT marker: %v", plural(len(generatedFileNames)), generatedFileNames) 500 } 501 502 // Write affected files. 503 var nerrs, npkgs int 504 for _, info := range r.packages { 505 first := true 506 for _, f := range info.Files { 507 tokenFile := r.iprog.Fset.File(f.Pos()) 508 if filesToUpdate[tokenFile] { 509 if first { 510 npkgs++ 511 first = false 512 if Verbose { 513 log.Printf("Updating package %s", info.Pkg.Path()) 514 } 515 } 516 517 filename := tokenFile.Name() 518 var buf bytes.Buffer 519 if err := format.Node(&buf, r.iprog.Fset, f); err != nil { 520 log.Printf("failed to pretty-print syntax tree: %v", err) 521 nerrs++ 522 continue 523 } 524 if err := writeFile(filename, buf.Bytes()); err != nil { 525 log.Print(err) 526 nerrs++ 527 } 528 } 529 } 530 } 531 if !Diff { 532 fmt.Printf("Renamed %d occurrence%s in %d file%s in %d package%s.\n", 533 nidents, plural(nidents), 534 len(filesToUpdate), plural(len(filesToUpdate)), 535 npkgs, plural(npkgs)) 536 } 537 if nerrs > 0 { 538 return fmt.Errorf("failed to rewrite %d file%s", nerrs, plural(nerrs)) 539 } 540 return nil 541 } 542 543 // docComment returns the doc for an identifier. 544 func (r *renamer) docComment(id *ast.Ident) *ast.CommentGroup { 545 _, nodes, _ := r.iprog.PathEnclosingInterval(id.Pos(), id.End()) 546 for _, node := range nodes { 547 switch decl := node.(type) { 548 case *ast.FuncDecl: 549 return decl.Doc 550 case *ast.Field: 551 return decl.Doc 552 case *ast.GenDecl: 553 return decl.Doc 554 // For {Type,Value}Spec, if the doc on the spec is absent, 555 // search for the enclosing GenDecl 556 case *ast.TypeSpec: 557 if decl.Doc != nil { 558 return decl.Doc 559 } 560 case *ast.ValueSpec: 561 if decl.Doc != nil { 562 return decl.Doc 563 } 564 case *ast.Ident: 565 default: 566 return nil 567 } 568 } 569 return nil 570 } 571 572 func plural(n int) string { 573 if n != 1 { 574 return "s" 575 } 576 return "" 577 } 578 579 // writeFile is a seam for testing and for the -d flag. 580 var writeFile = reallyWriteFile 581 582 func reallyWriteFile(filename string, content []byte) error { 583 return ioutil.WriteFile(filename, content, 0644) 584 } 585 586 func diff(filename string, content []byte) error { 587 renamed := fmt.Sprintf("%s.%d.renamed", filename, os.Getpid()) 588 if err := ioutil.WriteFile(renamed, content, 0644); err != nil { 589 return err 590 } 591 defer os.Remove(renamed) 592 593 diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput() 594 if len(diff) > 0 { 595 // diff exits with a non-zero status when the files don't match. 596 // Ignore that failure as long as we get output. 597 stdout.Write(diff) 598 return nil 599 } 600 if err != nil { 601 return fmt.Errorf("computing diff: %v", err) 602 } 603 return nil 604 }