sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/loader/refs.go (about)

     1  /*
     2  Copyright 2019 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  	"strconv"
    23  	"sync"
    24  )
    25  
    26  // NB(directxman12): most of this is done by the typechecker,
    27  // but it's a bit slow/heavyweight for what we want -- we want
    28  // to resolve external imports *only* if we actually need them.
    29  
    30  // Basically, what we do is:
    31  // 1. Map imports to names
    32  // 2. Find all explicit external references (`name.type`)
    33  // 3. Find all referenced packages by merging explicit references and dot imports
    34  // 4. Only type-check those packages
    35  // 5. Ignore type-checking errors from the missing packages, because we won't ever
    36  //    touch unloaded types (they're probably used in ignored fields/types, variables, or functions)
    37  //    (done using PrintErrors with an ignore argument from the caller).
    38  // 6. Notice any actual type-checking errors via invalid types
    39  
    40  // importsMap saves import aliases, mapping them to underlying packages.
    41  type importsMap struct {
    42  	// dotImports maps package IDs to packages for any packages that have/ been imported as `.`
    43  	dotImports map[string]*Package
    44  	// byName maps package aliases or names to the underlying package.
    45  	byName map[string]*Package
    46  }
    47  
    48  // mapImports maps imports from the names they use in the given file to the underlying package,
    49  // using a map of package import paths to packages (generally from Package.Imports()).
    50  func mapImports(file *ast.File, importedPkgs map[string]*Package) (*importsMap, error) {
    51  	m := &importsMap{
    52  		dotImports: make(map[string]*Package),
    53  		byName:     make(map[string]*Package),
    54  	}
    55  	for _, importSpec := range file.Imports {
    56  		path, err := strconv.Unquote(importSpec.Path.Value)
    57  		if err != nil {
    58  			return nil, ErrFromNode(err, importSpec.Path)
    59  		}
    60  		importedPkg := importedPkgs[path]
    61  		if importedPkg == nil {
    62  			return nil, ErrFromNode(fmt.Errorf("no such package located"), importSpec.Path)
    63  		}
    64  		if importSpec.Name == nil {
    65  			m.byName[importedPkg.Name] = importedPkg
    66  			continue
    67  		}
    68  		if importSpec.Name.Name == "." {
    69  			m.dotImports[importedPkg.ID] = importedPkg
    70  			continue
    71  		}
    72  		m.byName[importSpec.Name.Name] = importedPkg
    73  	}
    74  
    75  	return m, nil
    76  }
    77  
    78  // referenceSet finds references to external packages' types in the given file,
    79  // without otherwise calling into the type-checker.  When checking structs,
    80  // it only checks fields with JSON tags.
    81  type referenceSet struct {
    82  	file    *ast.File
    83  	imports *importsMap
    84  	pkg     *Package
    85  
    86  	externalRefs map[*Package]struct{}
    87  }
    88  
    89  func (r *referenceSet) init() {
    90  	if r.externalRefs == nil {
    91  		r.externalRefs = make(map[*Package]struct{})
    92  	}
    93  }
    94  
    95  // NodeFilter filters nodes, accepting them for reference collection
    96  // when true is returned and rejecting them when false is returned.
    97  type NodeFilter func(ast.Node) bool
    98  
    99  // collectReferences saves all references to external types in the given info.
   100  func (r *referenceSet) collectReferences(rawType ast.Expr, filterNode NodeFilter) {
   101  	r.init()
   102  	col := &referenceCollector{
   103  		refs:       r,
   104  		filterNode: filterNode,
   105  	}
   106  	ast.Walk(col, rawType)
   107  }
   108  
   109  // external saves an external reference to the given named package.
   110  func (r *referenceSet) external(pkgName string) {
   111  	pkg := r.imports.byName[pkgName]
   112  	if pkg == nil {
   113  		r.pkg.AddError(fmt.Errorf("use of unimported package %q", pkgName))
   114  		return
   115  	}
   116  	r.externalRefs[pkg] = struct{}{}
   117  }
   118  
   119  // referenceCollector visits nodes in an AST, adding external references to a
   120  // referenceSet.
   121  type referenceCollector struct {
   122  	refs       *referenceSet
   123  	filterNode NodeFilter
   124  }
   125  
   126  func (c *referenceCollector) Visit(node ast.Node) ast.Visitor {
   127  	if !c.filterNode(node) {
   128  		return nil
   129  	}
   130  	switch typedNode := node.(type) {
   131  	case *ast.Ident:
   132  		// local reference or dot-import, ignore
   133  		return nil
   134  	case *ast.SelectorExpr:
   135  		switch x := typedNode.X.(type) {
   136  		case *ast.Ident:
   137  			pkgName := x.Name
   138  			c.refs.external(pkgName)
   139  			return nil
   140  		default:
   141  			return c
   142  		}
   143  	default:
   144  		return c
   145  	}
   146  }
   147  
   148  // allReferencedPackages finds all directly referenced packages in the given package.
   149  func allReferencedPackages(pkg *Package, filterNodes NodeFilter) []*Package {
   150  	pkg.NeedSyntax()
   151  	refsByFile := make(map[*ast.File]*referenceSet)
   152  	for _, file := range pkg.Syntax {
   153  		imports, err := mapImports(file, pkg.Imports())
   154  		if err != nil {
   155  			pkg.AddError(err)
   156  			return nil
   157  		}
   158  		refs := &referenceSet{
   159  			file:    file,
   160  			imports: imports,
   161  			pkg:     pkg,
   162  		}
   163  		refsByFile[file] = refs
   164  	}
   165  
   166  	EachType(pkg, func(file *ast.File, _ *ast.GenDecl, spec *ast.TypeSpec) {
   167  		refs := refsByFile[file]
   168  		refs.collectReferences(spec.Type, filterNodes)
   169  	})
   170  
   171  	allPackages := make(map[*Package]struct{})
   172  	for _, refs := range refsByFile {
   173  		for _, pkg := range refs.imports.dotImports {
   174  			allPackages[pkg] = struct{}{}
   175  		}
   176  		for ref := range refs.externalRefs {
   177  			allPackages[ref] = struct{}{}
   178  		}
   179  	}
   180  
   181  	res := make([]*Package, 0, len(allPackages))
   182  	for pkg := range allPackages {
   183  		res = append(res, pkg)
   184  	}
   185  	return res
   186  }
   187  
   188  // TypeChecker performs type-checking on a limitted subset of packages by
   189  // checking each package's types' externally-referenced types, and only
   190  // type-checking those packages.
   191  type TypeChecker struct {
   192  	// NodeFilters are used to filter the set of references that are followed
   193  	// when typechecking.  If any of the filters returns true for a given node,
   194  	// its package will be added to the set of packages to check.
   195  	//
   196  	// If no filters are specified, all references are followed (this may be slow).
   197  	//
   198  	// Modifying this after the first call to check may yield strange/invalid
   199  	// results.
   200  	NodeFilters []NodeFilter
   201  
   202  	checkedPackages map[*Package]struct{}
   203  	sync.Mutex
   204  }
   205  
   206  // Check type-checks the given package and all packages referenced by types
   207  // that pass through (have true returned by) any of the NodeFilters.
   208  func (c *TypeChecker) Check(root *Package) {
   209  	c.init()
   210  
   211  	// use a sub-checker with the appropriate settings
   212  	(&TypeChecker{
   213  		NodeFilters:     c.NodeFilters,
   214  		checkedPackages: c.checkedPackages,
   215  	}).check(root)
   216  }
   217  
   218  func (c *TypeChecker) isNodeInteresting(node ast.Node) bool {
   219  	// no filters --> everything is important
   220  	if len(c.NodeFilters) == 0 {
   221  		return true
   222  	}
   223  
   224  	// otherwise, passing through any one filter means this node is important
   225  	for _, filter := range c.NodeFilters {
   226  		if filter(node) {
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  func (c *TypeChecker) init() {
   234  	if c.checkedPackages == nil {
   235  		c.checkedPackages = make(map[*Package]struct{})
   236  	}
   237  }
   238  
   239  // check recursively type-checks the given package, only loading packages that
   240  // are actually referenced by our types (it's the actual implementation of Check,
   241  // without initialization).
   242  func (c *TypeChecker) check(root *Package) {
   243  	root.Lock()
   244  	defer root.Unlock()
   245  
   246  	c.Lock()
   247  	_, ok := c.checkedPackages[root]
   248  	c.Unlock()
   249  	if ok {
   250  		return
   251  	}
   252  
   253  	refedPackages := allReferencedPackages(root, c.isNodeInteresting)
   254  
   255  	// first, resolve imports for all leaf packages...
   256  	var wg sync.WaitGroup
   257  	for _, pkg := range refedPackages {
   258  		wg.Add(1)
   259  		go func(pkg *Package) {
   260  			defer wg.Done()
   261  			c.check(pkg)
   262  		}(pkg)
   263  	}
   264  	wg.Wait()
   265  
   266  	// ...then, we can safely type-check ourself
   267  	root.NeedTypesInfo()
   268  
   269  	c.Lock()
   270  	defer c.Unlock()
   271  	c.checkedPackages[root] = struct{}{}
   272  }