sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/loader/loader.go (about) 1 /* 2 Copyright 2019-2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package loader 18 19 import ( 20 "fmt" 21 "go/ast" 22 "go/parser" 23 "go/scanner" 24 "go/token" 25 "go/types" 26 "os" 27 "path/filepath" 28 "regexp" 29 "sync" 30 31 "golang.org/x/tools/go/packages" 32 "k8s.io/apimachinery/pkg/util/sets" 33 ) 34 35 // Much of this is strongly inspired by the contents of go/packages, 36 // except that it allows for lazy loading of syntax and type-checking 37 // information to speed up cases where full traversal isn't needed. 38 39 // PrintErrors print errors associated with all packages 40 // in the given package graph, starting at the given root 41 // packages and traversing through all imports. It will skip 42 // any errors of the kinds specified in filterKinds. It will 43 // return true if any errors were printed. 44 func PrintErrors(pkgs []*Package, filterKinds ...packages.ErrorKind) bool { 45 pkgsRaw := make([]*packages.Package, len(pkgs)) 46 for i, pkg := range pkgs { 47 pkgsRaw[i] = pkg.Package 48 } 49 toSkip := make(map[packages.ErrorKind]struct{}) 50 for _, errKind := range filterKinds { 51 toSkip[errKind] = struct{}{} 52 } 53 hadErrors := false 54 packages.Visit(pkgsRaw, nil, func(pkgRaw *packages.Package) { 55 for _, err := range pkgRaw.Errors { 56 if _, skip := toSkip[err.Kind]; skip { 57 continue 58 } 59 hadErrors = true 60 fmt.Fprintln(os.Stderr, err) 61 } 62 }) 63 return hadErrors 64 } 65 66 // Package is a single, unique Go package that can be 67 // lazily parsed and type-checked. Packages should not 68 // be constructed directly -- instead, use LoadRoots. 69 // For a given call to LoadRoots, only a single instance 70 // of each package exists, and thus they may be used as keys 71 // and for comparison. 72 type Package struct { 73 *packages.Package 74 75 imports map[string]*Package 76 77 loader *loader 78 sync.Mutex 79 } 80 81 // Imports returns the imports for the given package, indexed by 82 // package path (*not* name in any particular file). 83 func (p *Package) Imports() map[string]*Package { 84 if p.imports == nil { 85 p.imports = p.loader.packagesFor(p.Package.Imports) 86 } 87 88 return p.imports 89 } 90 91 // NeedTypesInfo indicates that type-checking information is needed for this package. 92 // Actual type-checking information can be accessed via the Types and TypesInfo fields. 93 func (p *Package) NeedTypesInfo() { 94 if p.TypesInfo != nil { 95 return 96 } 97 p.NeedSyntax() 98 p.loader.typeCheck(p) 99 } 100 101 // NeedSyntax indicates that a parsed AST is needed for this package. 102 // Actual ASTs can be accessed via the Syntax field. 103 func (p *Package) NeedSyntax() { 104 if p.Syntax != nil { 105 return 106 } 107 out := make([]*ast.File, len(p.CompiledGoFiles)) 108 var wg sync.WaitGroup 109 wg.Add(len(p.CompiledGoFiles)) 110 for i, filename := range p.CompiledGoFiles { 111 go func(i int, filename string) { 112 defer wg.Done() 113 src, err := os.ReadFile(filename) 114 if err != nil { 115 p.AddError(err) 116 return 117 } 118 out[i], err = p.loader.parseFile(filename, src) 119 if err != nil { 120 p.AddError(err) 121 return 122 } 123 }(i, filename) 124 } 125 wg.Wait() 126 for _, file := range out { 127 if file == nil { 128 return 129 } 130 } 131 p.Syntax = out 132 } 133 134 // AddError adds an error to the errors associated with the given package. 135 func (p *Package) AddError(err error) { 136 switch typedErr := err.(type) { 137 case *os.PathError: 138 // file-reading errors 139 p.Errors = append(p.Errors, packages.Error{ 140 Pos: typedErr.Path + ":1", 141 Msg: typedErr.Err.Error(), 142 Kind: packages.ParseError, 143 }) 144 case scanner.ErrorList: 145 // parsing/scanning errors 146 for _, subErr := range typedErr { 147 p.Errors = append(p.Errors, packages.Error{ 148 Pos: subErr.Pos.String(), 149 Msg: subErr.Msg, 150 Kind: packages.ParseError, 151 }) 152 } 153 case types.Error: 154 // type-checking errors 155 p.Errors = append(p.Errors, packages.Error{ 156 Pos: typedErr.Fset.Position(typedErr.Pos).String(), 157 Msg: typedErr.Msg, 158 Kind: packages.TypeError, 159 }) 160 case ErrList: 161 for _, subErr := range typedErr { 162 p.AddError(subErr) 163 } 164 case PositionedError: 165 p.Errors = append(p.Errors, packages.Error{ 166 Pos: p.loader.cfg.Fset.Position(typedErr.Pos).String(), 167 Msg: typedErr.Error(), 168 Kind: packages.UnknownError, 169 }) 170 default: 171 // should only happen for external errors, like ref checking 172 p.Errors = append(p.Errors, packages.Error{ 173 Pos: p.ID + ":-", 174 Msg: err.Error(), 175 Kind: packages.UnknownError, 176 }) 177 } 178 } 179 180 // loader loads packages and their imports. Loaded packages will have 181 // type size, imports, and exports file information populated. Additional 182 // information, like ASTs and type-checking information, can be accessed 183 // via methods on individual packages. 184 type loader struct { 185 // Roots are the loaded "root" packages in the package graph loaded via 186 // LoadRoots. 187 Roots []*Package 188 189 // cfg contains the package loading config (initialized on demand) 190 cfg *packages.Config 191 // packages contains the cache of Packages indexed by the underlying 192 // package.Package, so that we don't ever produce two Packages with 193 // the same underlying packages.Package. 194 packages map[*packages.Package]*Package 195 packagesMu sync.Mutex 196 } 197 198 // packageFor returns a wrapped Package for the given packages.Package, 199 // ensuring that there's a one-to-one mapping between the two. 200 // It's *not* threadsafe -- use packagesFor for that. 201 func (l *loader) packageFor(pkgRaw *packages.Package) *Package { 202 if l.packages[pkgRaw] == nil { 203 l.packages[pkgRaw] = &Package{ 204 Package: pkgRaw, 205 loader: l, 206 } 207 } 208 return l.packages[pkgRaw] 209 } 210 211 // packagesFor returns a map of Package objects for each packages.Package in the input 212 // map, ensuring that there's a one-to-one mapping between package.Package and Package 213 // (as per packageFor). 214 func (l *loader) packagesFor(pkgsRaw map[string]*packages.Package) map[string]*Package { 215 l.packagesMu.Lock() 216 defer l.packagesMu.Unlock() 217 218 out := make(map[string]*Package, len(pkgsRaw)) 219 for name, rawPkg := range pkgsRaw { 220 out[name] = l.packageFor(rawPkg) 221 } 222 return out 223 } 224 225 // typeCheck type-checks the given package. 226 func (l *loader) typeCheck(pkg *Package) { 227 // don't conflict with typeCheckFromExportData 228 229 pkg.TypesInfo = &types.Info{ 230 Types: make(map[ast.Expr]types.TypeAndValue), 231 Defs: make(map[*ast.Ident]types.Object), 232 Uses: make(map[*ast.Ident]types.Object), 233 Implicits: make(map[ast.Node]types.Object), 234 Scopes: make(map[ast.Node]*types.Scope), 235 Selections: make(map[*ast.SelectorExpr]*types.Selection), 236 } 237 238 pkg.Fset = l.cfg.Fset 239 pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name) 240 241 importer := importerFunc(func(path string) (*types.Package, error) { 242 if path == "unsafe" { 243 return types.Unsafe, nil 244 } 245 246 // The imports map is keyed by import path. 247 importedPkg := pkg.Imports()[path] 248 if importedPkg == nil { 249 return nil, fmt.Errorf("package %q possibly creates an import loop", path) 250 } 251 252 // it's possible to have a call to check in parallel to a call to this 253 // if one package in the package graph gets its dependency filtered out, 254 // but another doesn't (so one wants a "placeholder" package here, and another 255 // wants the full check). 256 // 257 // Thus, we need to lock here (at least for the time being) to avoid 258 // races between the above write to `pkg.Types` and this checking of 259 // importedPkg.Types. 260 importedPkg.Lock() 261 defer importedPkg.Unlock() 262 263 if importedPkg.Types != nil && importedPkg.Types.Complete() { 264 return importedPkg.Types, nil 265 } 266 267 // if we haven't already loaded typecheck data, we don't care about this package's types 268 return types.NewPackage(importedPkg.PkgPath, importedPkg.Name), nil 269 }) 270 271 var errs []error 272 273 // type-check 274 checkConfig := &types.Config{ 275 Importer: importer, 276 277 IgnoreFuncBodies: true, // we only need decl-level info 278 279 Error: func(err error) { 280 errs = append(errs, err) 281 }, 282 283 Sizes: pkg.TypesSizes, 284 } 285 if err := types.NewChecker(checkConfig, l.cfg.Fset, pkg.Types, pkg.TypesInfo).Files(pkg.Syntax); err != nil { 286 errs = append(errs, err) 287 } 288 289 // make sure that if a given sub-import is ill-typed, we mark this package as ill-typed as well. 290 illTyped := len(errs) > 0 291 if !illTyped { 292 for _, importedPkg := range pkg.Imports() { 293 if importedPkg.IllTyped { 294 illTyped = true 295 break 296 } 297 } 298 } 299 pkg.IllTyped = illTyped 300 301 // publish errors to the package error list. 302 for _, err := range errs { 303 pkg.AddError(err) 304 } 305 } 306 307 // parseFile parses the given file, including comments. 308 func (l *loader) parseFile(filename string, src []byte) (*ast.File, error) { 309 // skip function bodies 310 file, err := parser.ParseFile(l.cfg.Fset, filename, src, parser.AllErrors|parser.ParseComments) 311 if err != nil { 312 return nil, err 313 } 314 315 return file, nil 316 } 317 318 // LoadRoots loads the given "root" packages by path, transitively loading 319 // and all imports as well. 320 // 321 // Loaded packages will have type size, imports, and exports file information 322 // populated. Additional information, like ASTs and type-checking information, 323 // can be accessed via methods on individual packages. 324 func LoadRoots(roots ...string) ([]*Package, error) { 325 return LoadRootsWithConfig(&packages.Config{}, roots...) 326 } 327 328 // LoadRootsWithConfig functions like LoadRoots, except that it allows passing 329 // a custom loading config. The config will be modified to suit the needs of 330 // the loader. 331 // 332 // This is generally only useful for use in testing when you need to modify 333 // loading settings to load from a fake location. 334 // 335 // This function will traverse Go module boundaries for roots that are file- 336 // system paths and end with "...". Please note this feature currently only 337 // supports roots that are filesystem paths. For more information, please 338 // refer to the high-level outline of this function's logic: 339 // 340 // 1. If no roots are provided then load the working directory and return 341 // early. 342 // 343 // 2. Otherwise sort the provided roots into two, distinct buckets: 344 // 345 // a. package/module names 346 // b. filesystem paths 347 // 348 // A filesystem path is distinguished from a Go package/module name by 349 // the same rules as followed by the "go" command. At a high level, a 350 // root is a filesystem path IFF it meets ANY of the following criteria: 351 // 352 // * is absolute 353 // * begins with . 354 // * begins with .. 355 // 356 // For more information please refer to the output of the command 357 // "go help packages". 358 // 359 // 3. Load the package/module roots as a single call to packages.Load. If 360 // there are no filesystem path roots then return early. 361 // 362 // 4. For filesystem path roots ending with "...", check to see if its 363 // descendants include any nested, Go modules. If so, add the directory 364 // that contains the nested Go module to the filesystem path roots. 365 // 366 // 5. Load the filesystem path roots and return the load packages for the 367 // package/module roots AND the filesystem path roots. 368 func LoadRootsWithConfig(cfg *packages.Config, roots ...string) ([]*Package, error) { 369 l := &loader{ 370 cfg: cfg, 371 packages: make(map[*packages.Package]*Package), 372 } 373 l.cfg.Mode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypesSizes 374 if l.cfg.Fset == nil { 375 l.cfg.Fset = token.NewFileSet() 376 } 377 // put our build flags first so that callers can override them 378 l.cfg.BuildFlags = append([]string{"-tags", "ignore_autogenerated"}, l.cfg.BuildFlags...) 379 380 // Visit the import graphs of the loaded, root packages. If an imported 381 // package refers to another loaded, root package, then replace the 382 // instance of the imported package with a reference to the loaded, root 383 // package. This is required to make kubebuilder markers work correctly 384 // when multiple root paths are loaded and types from one path reference 385 // types from another root path. 386 defer func() { 387 for i := range l.Roots { 388 visitImports(l.Roots, l.Roots[i], nil) 389 } 390 }() 391 392 // uniquePkgIDs is used to keep track of the discovered packages to be nice 393 // and try and prevent packages from showing up twice when nested module 394 // support is enabled. there is not harm that comes from this per se, but 395 // it makes testing easier when a known number of modules can be asserted 396 uniquePkgIDs := sets.Set[string]{} 397 398 // loadPackages returns the Go packages for the provided roots 399 // 400 // if validatePkgFn is nil, a package will be returned in the slice, 401 // otherwise the package is only returned if the result of 402 // validatePkgFn(pkg.ID) is truthy 403 loadPackages := func(roots ...string) ([]*Package, error) { 404 rawPkgs, err := packages.Load(l.cfg, roots...) 405 if err != nil { 406 loadRoot := l.cfg.Dir 407 if l.cfg.Dir == "" { 408 loadRoot, _ = os.Getwd() 409 } 410 return nil, fmt.Errorf("load packages in root %q: %w", loadRoot, err) 411 } 412 var pkgs []*Package 413 for _, rp := range rawPkgs { 414 p := l.packageFor(rp) 415 if !uniquePkgIDs.Has(p.ID) { 416 pkgs = append(pkgs, p) 417 uniquePkgIDs.Insert(p.ID) 418 } 419 } 420 return pkgs, nil 421 } 422 423 // if no roots were provided then load the current package and return early 424 if len(roots) == 0 { 425 pkgs, err := loadPackages() 426 if err != nil { 427 return nil, err 428 } 429 l.Roots = append(l.Roots, pkgs...) 430 return l.Roots, nil 431 } 432 433 // pkgRoots is a slice of roots that are package/modules and fspRoots 434 // is a slice of roots that are local filesystem paths. 435 // 436 // please refer to this function's godoc comments for more information on 437 // how these two types of roots are distinguished from one another 438 var ( 439 pkgRoots []string 440 fspRoots []string 441 fspRootRx = regexp.MustCompile(`^\.{1,2}`) 442 ) 443 for _, r := range roots { 444 if filepath.IsAbs(r) || fspRootRx.MatchString(r) { 445 fspRoots = append(fspRoots, r) 446 } else { 447 pkgRoots = append(pkgRoots, r) 448 } 449 } 450 451 // handle the package roots by sending them into the packages.Load function 452 // all at once. this is more efficient, but cannot be used for the file- 453 // system path roots due to them needing a custom, calculated value for the 454 // cfg.Dir field 455 if len(pkgRoots) > 0 { 456 pkgs, err := loadPackages(pkgRoots...) 457 if err != nil { 458 return nil, err 459 } 460 l.Roots = append(l.Roots, pkgs...) 461 } 462 463 // if there are no filesystem path roots then go ahead and return early 464 if len(fspRoots) == 0 { 465 return l.Roots, nil 466 } 467 468 // 469 // at this point we are handling filesystem path roots 470 // 471 472 // ensure the cfg.Dir field is reset to its original value upon 473 // returning from this function. it should honestly be fine if it is 474 // not given most callers will not send in the cfg parameter directly, 475 // as it's largely for testing, but still, let's be good stewards. 476 defer func(d string) { 477 cfg.Dir = d 478 }(cfg.Dir) 479 480 // store the value of cfg.Dir so we can use it later if it is non-empty. 481 // we need to store it now as the value of cfg.Dir will be updated by 482 // a loop below 483 cfgDir := cfg.Dir 484 485 // addNestedGoModulesToRoots is given to filepath.WalkDir and adds the 486 // directory part of p to the list of filesystem path roots IFF p is the 487 // path to a file named "go.mod" 488 addNestedGoModulesToRoots := func( 489 p string, 490 d os.DirEntry, 491 e error) error { 492 if e != nil { 493 return e 494 } 495 if !d.IsDir() && filepath.Base(p) == "go.mod" { 496 fspRoots = append(fspRoots, filepath.Join(filepath.Dir(p), "...")) 497 } 498 return nil 499 } 500 501 // in the first pass over the filesystem path roots we: 502 // 503 // 1. make the root into an absolute path 504 // 505 // 2. check to see if a root uses the nested path syntax, ex. ... 506 // 507 // 3. if so, walk the root's descendants, searching for any nested Go 508 // modules 509 // 510 // 4. if found then the directory containing the Go module is added to 511 // the list of the filesystem path roots 512 for i := range fspRoots { 513 r := fspRoots[i] 514 515 // clean up the root 516 r = filepath.Clean(r) 517 518 // get the absolute path of the root 519 if !filepath.IsAbs(r) { 520 // if the initial value of cfg.Dir was non-empty then use it when 521 // building the absolute path to this root. otherwise use the 522 // filepath.Abs function to get the absolute path of the root based 523 // on the working directory 524 if cfgDir != "" { 525 r = filepath.Join(cfgDir, r) 526 } else { 527 ar, err := filepath.Abs(r) 528 if err != nil { 529 return nil, err 530 } 531 r = ar 532 } 533 } 534 535 // update the root to be an absolute path 536 fspRoots[i] = r 537 538 b, d := filepath.Base(r), filepath.Dir(r) 539 540 // if the base element is "..." then it means nested traversal is 541 // activated. this can be passed directly to the loader. however, if 542 // specified we also want to traverse the path manually to determine if 543 // there are any nested Go modules we want to add to the list of file- 544 // system path roots to process 545 if b == "..." { 546 if err := filepath.WalkDir( 547 d, 548 addNestedGoModulesToRoots); err != nil { 549 return nil, err 550 } 551 } 552 } 553 554 // in the second pass over the filesystem path roots we: 555 // 556 // 1. determine the directory from which to execute the loader 557 // 558 // 2. update the loader config's Dir property to be the directory from 559 // step one 560 // 561 // 3. determine whether the root passed to the loader should be "./." 562 // or "./..." 563 // 564 // 4. execute the loader with the value from step three 565 for _, r := range fspRoots { 566 b, d := filepath.Base(r), filepath.Dir(r) 567 568 // we want the base part of the path to be either "..." or ".", except 569 // Go's filepath utilities clean paths during manipulation, removing the 570 // ".". thus, if not "...", let's update the path components so that: 571 // 572 // d = r 573 // b = "." 574 if b != "..." { 575 d = r 576 b = "." 577 } 578 579 // update the loader configuration's Dir field to the directory part of 580 // the root 581 l.cfg.Dir = d 582 583 // update the root to be "./..." or "./." 584 // (with OS-specific filepath separator). please note filepath.Join 585 // would clean up the trailing "." character that we want preserved, 586 // hence the more manual path concatenation logic 587 r = fmt.Sprintf(".%s%s", string(filepath.Separator), b) 588 589 // load the packages from the roots 590 pkgs, err := loadPackages(r) 591 if err != nil { 592 return nil, err 593 } 594 l.Roots = append(l.Roots, pkgs...) 595 } 596 597 return l.Roots, nil 598 } 599 600 // visitImports walks a dependency graph, replacing imported package 601 // references with those from the rootPkgs list. This ensures the 602 // kubebuilder marker generation is handled correctly. For more info, 603 // please see issue 680. 604 func visitImports(rootPkgs []*Package, pkg *Package, seen sets.Set[string]) { 605 if seen == nil { 606 seen = sets.Set[string]{} 607 } 608 for importedPkgID, importedPkg := range pkg.Imports() { 609 for i := range rootPkgs { 610 if importedPkgID == rootPkgs[i].ID { 611 pkg.imports[importedPkgID] = rootPkgs[i] 612 } 613 } 614 if !seen.Has(importedPkgID) { 615 seen.Insert(importedPkgID) 616 visitImports(rootPkgs, importedPkg, seen) 617 } 618 } 619 } 620 621 // importFunc is an implementation of the single-method 622 // types.Importer interface based on a function value. 623 type importerFunc func(path string) (*types.Package, error) 624 625 func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }