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