github.com/waynz0r/controller-tools@v0.4.1-0.20200916220028-16254aeef2d7/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  	checkedPackages map[*Package]struct{}
   189  	filterNodes     NodeFilter
   190  	sync.Mutex
   191  }
   192  
   193  // Check type-checks the given package and all packages referenced
   194  // by types that pass through (have true returned by) filterNodes.
   195  func (c *TypeChecker) Check(root *Package, filterNodes NodeFilter) {
   196  	c.init()
   197  
   198  	if filterNodes == nil {
   199  		filterNodes = c.filterNodes
   200  	}
   201  
   202  	// use a sub-checker with the appropriate settings
   203  	(&TypeChecker{
   204  		filterNodes:     filterNodes,
   205  		checkedPackages: c.checkedPackages,
   206  	}).check(root)
   207  }
   208  
   209  func (c *TypeChecker) init() {
   210  	if c.checkedPackages == nil {
   211  		c.checkedPackages = make(map[*Package]struct{})
   212  	}
   213  	if c.filterNodes == nil {
   214  		// check every type by default
   215  		c.filterNodes = func(_ ast.Node) bool {
   216  			return true
   217  		}
   218  	}
   219  }
   220  
   221  // check recursively type-checks the given package, only loading packages that
   222  // are actually referenced by our types (it's the actual implementation of Check,
   223  // without initialization).
   224  func (c *TypeChecker) check(root *Package) {
   225  	root.Lock()
   226  	defer root.Unlock()
   227  
   228  	c.Lock()
   229  	_, ok := c.checkedPackages[root]
   230  	c.Unlock()
   231  	if ok {
   232  		return
   233  	}
   234  
   235  	refedPackages := allReferencedPackages(root, c.filterNodes)
   236  
   237  	// first, resolve imports for all leaf packages...
   238  	var wg sync.WaitGroup
   239  	for _, pkg := range refedPackages {
   240  		wg.Add(1)
   241  		go func(pkg *Package) {
   242  			defer wg.Done()
   243  			c.check(pkg)
   244  		}(pkg)
   245  	}
   246  	wg.Wait()
   247  
   248  	// ...then, we can safely type-check ourself
   249  	root.NeedTypesInfo()
   250  
   251  	c.Lock()
   252  	defer c.Unlock()
   253  	c.checkedPackages[root] = struct{}{}
   254  }