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 }