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