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) }