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