github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/refactor/rename/spec.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 6 7 // This file contains logic related to specifying a renaming: parsing of 8 // the flags as a form of query, and finding the object(s) it denotes. 9 // See Usage for flag details. 10 11 import ( 12 "bytes" 13 "fmt" 14 "go/ast" 15 "go/build" 16 "go/parser" 17 "go/token" 18 "go/types" 19 "log" 20 "os" 21 "path/filepath" 22 "regexp" 23 "strconv" 24 "strings" 25 26 "golang.org/x/tools/go/buildutil" 27 "golang.org/x/tools/go/loader" 28 ) 29 30 // A spec specifies an entity to rename. 31 // 32 // It is populated from an -offset flag or -from query; 33 // see Usage for the allowed -from query forms. 34 type spec struct { 35 // pkg is the package containing the position 36 // specified by the -from or -offset flag. 37 // If filename == "", our search for the 'from' entity 38 // is restricted to this package. 39 pkg string 40 41 // The original name of the entity being renamed. 42 // If the query had a ::from component, this is that; 43 // otherwise it's the last segment, e.g. 44 // (encoding/json.Decoder).from 45 // encoding/json.from 46 fromName string 47 48 // -- The remaining fields are private to this file. All are optional. -- 49 50 // The query's ::x suffix, if any. 51 searchFor string 52 53 // e.g. "Decoder" in "(encoding/json.Decoder).fieldOrMethod" 54 // or "encoding/json.Decoder 55 pkgMember string 56 57 // e.g. fieldOrMethod in "(encoding/json.Decoder).fieldOrMethod" 58 typeMember string 59 60 // Restricts the query to this file. 61 // Implied by -from="file.go::x" and -offset flags. 62 filename string 63 64 // Byte offset of the 'from' identifier within the file named 'filename'. 65 // -offset mode only. 66 offset int 67 } 68 69 // parseFromFlag interprets the "-from" flag value as a renaming specification. 70 // See Usage in rename.go for valid formats. 71 func parseFromFlag(ctxt *build.Context, fromFlag string) (*spec, error) { 72 var spec spec 73 var main string // sans "::x" suffix 74 switch parts := strings.Split(fromFlag, "::"); len(parts) { 75 case 1: 76 main = parts[0] 77 case 2: 78 main = parts[0] 79 spec.searchFor = parts[1] 80 if parts[1] == "" { 81 // error 82 } 83 default: 84 return nil, fmt.Errorf("-from %q: invalid identifier specification (see -help for formats)", fromFlag) 85 } 86 87 if strings.HasSuffix(main, ".go") { 88 // main is "filename.go" 89 if spec.searchFor == "" { 90 return nil, fmt.Errorf("-from: filename %q must have a ::name suffix", main) 91 } 92 spec.filename = main 93 if !buildutil.FileExists(ctxt, spec.filename) { 94 return nil, fmt.Errorf("no such file: %s", spec.filename) 95 } 96 97 bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename) 98 if err != nil { 99 return nil, err 100 } 101 spec.pkg = bp.ImportPath 102 103 } else { 104 // main is one of: 105 // "importpath" 106 // "importpath".member 107 // (*"importpath".type).fieldormethod (parens and star optional) 108 if err := parseObjectSpec(&spec, main); err != nil { 109 return nil, err 110 } 111 } 112 113 if spec.searchFor != "" { 114 spec.fromName = spec.searchFor 115 } 116 117 cwd, err := os.Getwd() 118 if err != nil { 119 return nil, err 120 } 121 122 // Sanitize the package. 123 bp, err := ctxt.Import(spec.pkg, cwd, build.FindOnly) 124 if err != nil { 125 return nil, fmt.Errorf("can't find package %q", spec.pkg) 126 } 127 spec.pkg = bp.ImportPath 128 129 if !isValidIdentifier(spec.fromName) { 130 return nil, fmt.Errorf("-from: invalid identifier %q", spec.fromName) 131 } 132 133 if Verbose { 134 log.Printf("-from spec: %+v", spec) 135 } 136 137 return &spec, nil 138 } 139 140 // parseObjectSpec parses main as one of the non-filename forms of 141 // object specification. 142 func parseObjectSpec(spec *spec, main string) error { 143 // Parse main as a Go expression, albeit a strange one. 144 e, _ := parser.ParseExpr(main) 145 146 if pkg := parseImportPath(e); pkg != "" { 147 // e.g. bytes or "encoding/json": a package 148 spec.pkg = pkg 149 if spec.searchFor == "" { 150 return fmt.Errorf("-from %q: package import path %q must have a ::name suffix", 151 main, main) 152 } 153 return nil 154 } 155 156 if e, ok := e.(*ast.SelectorExpr); ok { 157 x := unparen(e.X) 158 159 // Strip off star constructor, if any. 160 if star, ok := x.(*ast.StarExpr); ok { 161 x = star.X 162 } 163 164 if pkg := parseImportPath(x); pkg != "" { 165 // package member e.g. "encoding/json".HTMLEscape 166 spec.pkg = pkg // e.g. "encoding/json" 167 spec.pkgMember = e.Sel.Name // e.g. "HTMLEscape" 168 spec.fromName = e.Sel.Name 169 return nil 170 } 171 172 if x, ok := x.(*ast.SelectorExpr); ok { 173 // field/method of type e.g. ("encoding/json".Decoder).Decode 174 y := unparen(x.X) 175 if pkg := parseImportPath(y); pkg != "" { 176 spec.pkg = pkg // e.g. "encoding/json" 177 spec.pkgMember = x.Sel.Name // e.g. "Decoder" 178 spec.typeMember = e.Sel.Name // e.g. "Decode" 179 spec.fromName = e.Sel.Name 180 return nil 181 } 182 } 183 } 184 185 return fmt.Errorf("-from %q: invalid expression", main) 186 } 187 188 // parseImportPath returns the import path of the package denoted by e. 189 // Any import path may be represented as a string literal; 190 // single-segment import paths (e.g. "bytes") may also be represented as 191 // ast.Ident. parseImportPath returns "" for all other expressions. 192 func parseImportPath(e ast.Expr) string { 193 switch e := e.(type) { 194 case *ast.Ident: 195 return e.Name // e.g. bytes 196 197 case *ast.BasicLit: 198 if e.Kind == token.STRING { 199 pkgname, _ := strconv.Unquote(e.Value) 200 return pkgname // e.g. "encoding/json" 201 } 202 } 203 return "" 204 } 205 206 // parseOffsetFlag interprets the "-offset" flag value as a renaming specification. 207 func parseOffsetFlag(ctxt *build.Context, offsetFlag string) (*spec, error) { 208 var spec spec 209 // Validate -offset, e.g. file.go:#123 210 parts := strings.Split(offsetFlag, ":#") 211 if len(parts) != 2 { 212 return nil, fmt.Errorf("-offset %q: invalid offset specification", offsetFlag) 213 } 214 215 spec.filename = parts[0] 216 if !buildutil.FileExists(ctxt, spec.filename) { 217 return nil, fmt.Errorf("no such file: %s", spec.filename) 218 } 219 220 bp, err := buildutil.ContainingPackage(ctxt, wd, spec.filename) 221 if err != nil { 222 return nil, err 223 } 224 spec.pkg = bp.ImportPath 225 226 for _, r := range parts[1] { 227 if !isDigit(r) { 228 return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag) 229 } 230 } 231 spec.offset, err = strconv.Atoi(parts[1]) 232 if err != nil { 233 return nil, fmt.Errorf("-offset %q: non-numeric offset", offsetFlag) 234 } 235 236 // Parse the file and check there's an identifier at that offset. 237 fset := token.NewFileSet() 238 f, err := buildutil.ParseFile(fset, ctxt, nil, wd, spec.filename, parser.ParseComments) 239 if err != nil { 240 return nil, fmt.Errorf("-offset %q: cannot parse file: %s", offsetFlag, err) 241 } 242 243 id := identAtOffset(fset, f, spec.offset) 244 if id == nil { 245 return nil, fmt.Errorf("-offset %q: no identifier at this position", offsetFlag) 246 } 247 248 spec.fromName = id.Name 249 250 return &spec, nil 251 } 252 253 var wd = func() string { 254 wd, err := os.Getwd() 255 if err != nil { 256 panic("cannot get working directory: " + err.Error()) 257 } 258 return wd 259 }() 260 261 // For source trees built with 'go build', the -from or -offset 262 // spec identifies exactly one initial 'from' object to rename , 263 // but certain proprietary build systems allow a single file to 264 // appear in multiple packages (e.g. the test package contains a 265 // copy of its library), so there may be multiple objects for 266 // the same source entity. 267 268 func findFromObjects(iprog *loader.Program, spec *spec) ([]types.Object, error) { 269 if spec.filename != "" { 270 return findFromObjectsInFile(iprog, spec) 271 } 272 273 // Search for objects defined in specified package. 274 275 // TODO(adonovan): the iprog.ImportMap has an entry {"main": ...} 276 // for main packages, even though that's not an import path. 277 // Seems like a bug. 278 // 279 // pkg := iprog.ImportMap[spec.pkg] 280 // if pkg == nil { 281 // return fmt.Errorf("cannot find package %s", spec.pkg) // can't happen? 282 // } 283 // info := iprog.AllPackages[pkg] 284 285 // Workaround: lookup by value. 286 var info *loader.PackageInfo 287 var pkg *types.Package 288 for pkg, info = range iprog.AllPackages { 289 if pkg.Path() == spec.pkg { 290 break 291 } 292 } 293 if info == nil { 294 return nil, fmt.Errorf("package %q was not loaded", spec.pkg) 295 } 296 297 objects, err := findObjects(info, spec) 298 if err != nil { 299 return nil, err 300 } 301 if len(objects) > 1 { 302 // ambiguous "*" scope query 303 return nil, ambiguityError(iprog.Fset, objects) 304 } 305 return objects, nil 306 } 307 308 func findFromObjectsInFile(iprog *loader.Program, spec *spec) ([]types.Object, error) { 309 var fromObjects []types.Object 310 for _, info := range iprog.AllPackages { 311 // restrict to specified filename 312 // NB: under certain proprietary build systems, a given 313 // filename may appear in multiple packages. 314 for _, f := range info.Files { 315 thisFile := iprog.Fset.File(f.Pos()) 316 if !sameFile(thisFile.Name(), spec.filename) { 317 continue 318 } 319 // This package contains the query file. 320 321 if spec.offset != 0 { 322 // We cannot refactor generated files since position information is invalidated. 323 if generated(f, thisFile) { 324 return nil, fmt.Errorf("cannot rename identifiers in generated file containing DO NOT EDIT marker: %s", thisFile.Name()) 325 } 326 327 // Search for a specific ident by file/offset. 328 id := identAtOffset(iprog.Fset, f, spec.offset) 329 if id == nil { 330 // can't happen? 331 return nil, fmt.Errorf("identifier not found") 332 } 333 obj := info.Uses[id] 334 if obj == nil { 335 obj = info.Defs[id] 336 if obj == nil { 337 // Ident without Object. 338 339 // Package clause? 340 pos := thisFile.Pos(spec.offset) 341 _, path, _ := iprog.PathEnclosingInterval(pos, pos) 342 if len(path) == 2 { // [Ident File] 343 // TODO(adonovan): support this case. 344 return nil, fmt.Errorf("cannot rename %q: renaming package clauses is not yet supported", 345 path[1].(*ast.File).Name.Name) 346 } 347 348 // Implicit y in "switch y := x.(type) {"? 349 if obj := typeSwitchVar(&info.Info, path); obj != nil { 350 return []types.Object{obj}, nil 351 } 352 353 // Probably a type error. 354 return nil, fmt.Errorf("cannot find object for %q", id.Name) 355 } 356 } 357 if obj.Pkg() == nil { 358 return nil, fmt.Errorf("cannot rename predeclared identifiers (%s)", obj) 359 360 } 361 362 fromObjects = append(fromObjects, obj) 363 } else { 364 // do a package-wide query 365 objects, err := findObjects(info, spec) 366 if err != nil { 367 return nil, err 368 } 369 370 // filter results: only objects defined in thisFile 371 var filtered []types.Object 372 for _, obj := range objects { 373 if iprog.Fset.File(obj.Pos()) == thisFile { 374 filtered = append(filtered, obj) 375 } 376 } 377 if len(filtered) == 0 { 378 return nil, fmt.Errorf("no object %q declared in file %s", 379 spec.fromName, spec.filename) 380 } else if len(filtered) > 1 { 381 return nil, ambiguityError(iprog.Fset, filtered) 382 } 383 fromObjects = append(fromObjects, filtered[0]) 384 } 385 break 386 } 387 } 388 if len(fromObjects) == 0 { 389 // can't happen? 390 return nil, fmt.Errorf("file %s was not part of the loaded program", spec.filename) 391 } 392 return fromObjects, nil 393 } 394 395 func typeSwitchVar(info *types.Info, path []ast.Node) types.Object { 396 if len(path) > 3 { 397 // [Ident AssignStmt TypeSwitchStmt...] 398 if sw, ok := path[2].(*ast.TypeSwitchStmt); ok { 399 // choose the first case. 400 if len(sw.Body.List) > 0 { 401 obj := info.Implicits[sw.Body.List[0].(*ast.CaseClause)] 402 if obj != nil { 403 return obj 404 } 405 } 406 } 407 } 408 return nil 409 } 410 411 // On success, findObjects returns the list of objects named 412 // spec.fromName matching the spec. On success, the result has exactly 413 // one element unless spec.searchFor!="", in which case it has at least one 414 // element. 415 func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) { 416 if spec.pkgMember == "" { 417 if spec.searchFor == "" { 418 panic(spec) 419 } 420 objects := searchDefs(&info.Info, spec.searchFor) 421 if objects == nil { 422 return nil, fmt.Errorf("no object %q declared in package %q", 423 spec.searchFor, info.Pkg.Path()) 424 } 425 return objects, nil 426 } 427 428 pkgMember := info.Pkg.Scope().Lookup(spec.pkgMember) 429 if pkgMember == nil { 430 return nil, fmt.Errorf("package %q has no member %q", 431 info.Pkg.Path(), spec.pkgMember) 432 } 433 434 var searchFunc *types.Func 435 if spec.typeMember == "" { 436 // package member 437 if spec.searchFor == "" { 438 return []types.Object{pkgMember}, nil 439 } 440 441 // Search within pkgMember, which must be a function. 442 searchFunc, _ = pkgMember.(*types.Func) 443 if searchFunc == nil { 444 return nil, fmt.Errorf("cannot search for %q within %s %q", 445 spec.searchFor, objectKind(pkgMember), pkgMember) 446 } 447 } else { 448 // field/method of type 449 // e.g. (encoding/json.Decoder).Decode 450 // or ::x within it. 451 452 tName, _ := pkgMember.(*types.TypeName) 453 if tName == nil { 454 return nil, fmt.Errorf("%s.%s is a %s, not a type", 455 info.Pkg.Path(), pkgMember.Name(), objectKind(pkgMember)) 456 } 457 458 // search within named type. 459 obj, _, _ := types.LookupFieldOrMethod(tName.Type(), true, info.Pkg, spec.typeMember) 460 if obj == nil { 461 return nil, fmt.Errorf("cannot find field or method %q of %s %s.%s", 462 spec.typeMember, typeKind(tName.Type()), info.Pkg.Path(), tName.Name()) 463 } 464 465 if spec.searchFor == "" { 466 // If it is an embedded field, return the type of the field. 467 if v, ok := obj.(*types.Var); ok && v.Anonymous() { 468 switch t := v.Type().(type) { 469 case *types.Pointer: 470 return []types.Object{t.Elem().(*types.Named).Obj()}, nil 471 case *types.Named: 472 return []types.Object{t.Obj()}, nil 473 } 474 } 475 return []types.Object{obj}, nil 476 } 477 478 searchFunc, _ = obj.(*types.Func) 479 if searchFunc == nil { 480 return nil, fmt.Errorf("cannot search for local name %q within %s (%s.%s).%s; need a function", 481 spec.searchFor, objectKind(obj), info.Pkg.Path(), tName.Name(), 482 obj.Name()) 483 } 484 if isInterface(tName.Type()) { 485 return nil, fmt.Errorf("cannot search for local name %q within abstract method (%s.%s).%s", 486 spec.searchFor, info.Pkg.Path(), tName.Name(), searchFunc.Name()) 487 } 488 } 489 490 // -- search within function or method -- 491 492 decl := funcDecl(info, searchFunc) 493 if decl == nil { 494 return nil, fmt.Errorf("cannot find syntax for %s", searchFunc) // can't happen? 495 } 496 497 var objects []types.Object 498 for _, obj := range searchDefs(&info.Info, spec.searchFor) { 499 // We use positions, not scopes, to determine whether 500 // the obj is within searchFunc. This is clumsy, but the 501 // alternative, using the types.Scope tree, doesn't 502 // account for non-lexical objects like fields and 503 // interface methods. 504 if decl.Pos() <= obj.Pos() && obj.Pos() < decl.End() && obj != searchFunc { 505 objects = append(objects, obj) 506 } 507 } 508 if objects == nil { 509 return nil, fmt.Errorf("no local definition of %q within %s", 510 spec.searchFor, searchFunc) 511 } 512 return objects, nil 513 } 514 515 func funcDecl(info *loader.PackageInfo, fn *types.Func) *ast.FuncDecl { 516 for _, f := range info.Files { 517 for _, d := range f.Decls { 518 if d, ok := d.(*ast.FuncDecl); ok && info.Defs[d.Name] == fn { 519 return d 520 } 521 } 522 } 523 return nil 524 } 525 526 func searchDefs(info *types.Info, name string) []types.Object { 527 var objects []types.Object 528 for id, obj := range info.Defs { 529 if obj == nil { 530 // e.g. blank ident. 531 // TODO(adonovan): but also implicit y in 532 // switch y := x.(type) 533 // Needs some thought. 534 continue 535 } 536 if id.Name == name { 537 objects = append(objects, obj) 538 } 539 } 540 return objects 541 } 542 543 func identAtOffset(fset *token.FileSet, f *ast.File, offset int) *ast.Ident { 544 var found *ast.Ident 545 ast.Inspect(f, func(n ast.Node) bool { 546 if id, ok := n.(*ast.Ident); ok { 547 idpos := fset.Position(id.Pos()).Offset 548 if idpos <= offset && offset < idpos+len(id.Name) { 549 found = id 550 } 551 } 552 return found == nil // keep traversing only until found 553 }) 554 return found 555 } 556 557 // ambiguityError returns an error describing an ambiguous "*" scope query. 558 func ambiguityError(fset *token.FileSet, objects []types.Object) error { 559 var buf bytes.Buffer 560 for i, obj := range objects { 561 if i > 0 { 562 buf.WriteString(", ") 563 } 564 posn := fset.Position(obj.Pos()) 565 fmt.Fprintf(&buf, "%s at %s:%d:%d", 566 objectKind(obj), filepath.Base(posn.Filename), posn.Line, posn.Column) 567 } 568 return fmt.Errorf("ambiguous specifier %s matches %s", 569 objects[0].Name(), buf.String()) 570 } 571 572 // Matches cgo generated comment as well as the proposed standard: 573 // 574 // https://golang.org/s/generatedcode 575 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) 576 577 // generated reports whether ast.File is a generated file. 578 func generated(f *ast.File, tokenFile *token.File) bool { 579 580 // Iterate over the comments in the file 581 for _, commentGroup := range f.Comments { 582 for _, comment := range commentGroup.List { 583 if matched := generatedRx.MatchString(comment.Text); matched { 584 // Check if comment is at the beginning of the line in source 585 if pos := tokenFile.Position(comment.Slash); pos.Column == 1 { 586 return true 587 } 588 } 589 } 590 } 591 return false 592 }