/
pkginfo.go
358 lines (312 loc) · 9.77 KB
/
pkginfo.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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
// pkginfo keeps package information of an application for gottani.
// It also implements types.Importer for parsing files and checking types.
package pkginfo // import "github.com/ktateish/gottani/internal/pkginfo"
import (
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"io/ioutil"
"path/filepath"
)
// fakeCbpkg is *build.Pacakge for `import "C"`
var fakeCbpkg *build.Package
func init() {
fakeCbpkg = &build.Package{
Name: "C",
ImportPath: "C",
Dir: filepath.Join(build.Default.GOROOT, "src", "C"),
Goroot: true,
}
}
// key for maps
type pkgKey struct {
importPath string
dir string
}
// PackageInfo represents information of packages used by a applicaion for gottani.
// It also implements types.Importer for parsing and type-checking.
type PackageInfo struct {
// mapping (dir, importPath) => *build.Packages
pkgs map[pkgKey]*build.Package
// typesPkgs keeps mapping from *build.Package to *types.Package
typesPkgs map[*build.Package]*types.Package
// astFiles keeps mapping from *build.Package to []*ast.File
astFiles map[*build.Package][]*ast.File
fset *token.FileSet // sotre all files of whole application
tinfo *types.Info // store type information of whole application
rootPackage *build.Package
// memo for Pacakges() and AllPackages()
pkgSlice []*build.Package
allPkgSlice []*build.Package
}
// New creates PackageInfo with default setting and then Load the given dir
func New(dir string) (*PackageInfo, error) {
fset := token.NewFileSet()
tinfo := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
}
pi := NewPackageInfo(fset, tinfo)
err := pi.Load(dir)
return pi, err
}
// NewPackageInfo creates PackageInfo
func NewPackageInfo(fset *token.FileSet, tinfo *types.Info) *PackageInfo {
return &PackageInfo{
fset: token.NewFileSet(),
tinfo: tinfo,
pkgs: make(map[pkgKey]*build.Package),
typesPkgs: make(map[*build.Package]*types.Package),
astFiles: make(map[*build.Package][]*ast.File),
}
}
// Load loads package information of the application given by the dir
func (ip *PackageInfo) Load(dir string) error {
abs, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("getting absolute path %q: %w", dir, err)
}
bp, err := build.ImportDir(abs, 0)
if err != nil {
return fmt.Errorf("importing %q: %w", abs, err)
}
ip.rootPackage = bp
ip.pkgs[pkgKey{".", abs}] = bp
ip.pkgs[pkgKey{bp.ImportPath, "."}] = bp
tp, err := ip.typeCheck(bp)
if err != nil {
return fmt.Errorf("type checking: %w", err)
}
ip.typesPkgs[bp] = tp
return nil
}
// Packages returns a slice of *build.Packages that are depended by the loaded package and non standard ones.
func (ip *PackageInfo) Packages() []*build.Package {
if ip.pkgSlice != nil {
return ip.pkgSlice
}
var res []*build.Package
ip.WalkPackages(func(bp *build.Package) bool { return bp.Goroot }, nil, func(bp *build.Package, _ *types.Package, _ []*ast.File) {
res = append(res, bp)
})
ip.pkgSlice = res
return res
}
// Packages returns a slice of *build.Packages that are depended by the loaded package.
func (ip *PackageInfo) AllPackages() []*build.Package {
if ip.allPkgSlice != nil {
return ip.allPkgSlice
}
var res []*build.Package
ip.WalkPackages(nil, nil, func(bp *build.Package, _ *types.Package, _ []*ast.File) {
res = append(res, bp)
})
ip.allPkgSlice = res
return res
}
// Root() returns the package that is in the directory given on Load().
func (ip *PackageInfo) Root() *build.Package {
return ip.rootPackage
}
// FileSet() returns the *token.FileSet that holds all source code for whole the application except standard packages.
func (ip *PackageInfo) FileSet() *token.FileSet {
return ip.fset
}
// TypesInfo() returns the *types.Info that holds all type information for whole the application.
func (ip *PackageInfo) TypesInfo() *types.Info {
return ip.tinfo
}
// GetBuildPackage() returns the *build.Package coresponds for the given importPath on the given dir.
func (ip *PackageInfo) GetBuildPackage(importPath, dir string) *build.Package {
bp, err := ip.getBuildPackage(importPath, dir)
if err != nil {
panic(err)
}
return bp
}
// GetTypesPackage() returns the *type.Package for the package specified by the given bp.
func (ip *PackageInfo) GetTypesPackage(bp *build.Package) *types.Package {
return ip.typesPkgs[bp]
}
// GetTypesPackage() returns the slice of *ast.File for the package specified by the given bp.
// It includs Go files and Cgo files.
func (ip *PackageInfo) GetAstFiles(bp *build.Package) []*ast.File {
return ip.astFiles[bp]
}
// Walker is alias to a function type for PackageInfo.WalkPacakge().
type Walker func(bp *build.Package, tp *types.Package, asts []*ast.File)
// WalkPackages travarse packages and do the given pre and/or post function for each package
func (ip *PackageInfo) WalkPackages(prune func(*build.Package) bool, pre Walker, post Walker) {
visited := make(map[*build.Package]bool)
var rec func(bp *build.Package)
rec = func(bp *build.Package) {
if visited[bp] {
return
}
visited[bp] = true
if prune != nil && prune(bp) {
return
}
if pre != nil {
pre(bp, ip.typesPkgs[bp], ip.astFiles[bp])
}
for _, ipath := range bp.Imports {
next, err := ip.getBuildPackage(ipath, bp.ImportPath)
if err != nil {
msg := fmt.Sprintf("package %q not found on %q", ipath, bp.ImportPath)
panic(msg)
}
rec(next)
}
if post != nil {
post(bp, ip.typesPkgs[bp], ip.astFiles[bp])
}
}
rec(ip.rootPackage)
}
// getBuildPackage finds *build.Packages matched to the given importPath on the given dir.
func (pi *PackageInfo) getBuildPackage(importPath, dir string) (*build.Package, error) {
key := pkgKey{importPath, dir}
if bp, ok := pi.pkgs[key]; ok {
return bp, nil
}
var bp *build.Package
if importPath == "C" {
// Always returns fake package for importPath "C" on any directory because "C" package doesn't exist
bp = fakeCbpkg
} else {
abs, err := getImportDirAbs(importPath, dir)
absKey := pkgKey{".", abs}
var ok bool
bp, ok = pi.pkgs[absKey]
if !ok {
bp, err = build.ImportDir(abs, build.AllowBinary)
if err != nil {
return nil, err
}
bp.ImportPath = importPath
pi.pkgs[absKey] = bp
}
}
pi.pkgs[key] = bp
return bp, nil
}
// methods implements types.ImporterFrom and helper functions
// Import imports package specified by the path and returns its type information
func (ip *PackageInfo) Import(path string) (*types.Package, error) {
return ip.ImportFrom(path, ".", 0)
}
// Import imports package specified by the given path and dir, then returns its type information
// The mode must be set 0. It is reserved for future use.
func (ip *PackageInfo) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
bp, err := ip.getBuildPackage(path, dir)
if err != nil {
return nil, fmt.Errorf("getting *build.Package for %q on %q: %w", path, dir, err)
}
if tp, ok := ip.typesPkgs[bp]; ok {
return tp, nil
}
tp, err := ip.typeCheck(bp)
if err != nil {
return nil, err
}
ip.typesPkgs[bp] = tp
return tp, nil
}
func (ip *PackageInfo) typeCheck(bp *build.Package) (*types.Package, error) {
if bp.Goroot && bp.ImportPath == "unsafe" {
return types.Unsafe, nil
}
files, err := parsePackage(ip.fset, bp)
if err != nil {
return nil, fmt.Errorf("parsing package: %w", err)
}
var hardErrors, softErrors []error
tcfg := types.Config{
IgnoreFuncBodies: bp.Goroot, // doesn't check function body if the package is standard (in GOROOT/src)
FakeImportC: true,
Error: func(err error) {
if terr, ok := err.(types.Error); ok && !terr.Soft {
hardErrors = append(hardErrors, err)
} else {
softErrors = append(softErrors, err)
}
},
Importer: ip,
}
tp, err := tcfg.Check(bp.ImportPath, ip.fset, files, ip.tinfo)
if err != nil {
if 0 < len(hardErrors) {
return nil, fmt.Errorf("type checking: %w", hardErrors[0])
} else {
return nil, fmt.Errorf("type checking: %w", err)
}
}
var imps []*types.Package
for _, ipath := range bp.Imports {
ibp, err := ip.getBuildPackage(ipath, bp.ImportPath)
if err != nil {
return nil, fmt.Errorf("getting *build.Package for %q on %q: %w", ipath, bp.ImportPath, err)
}
p, ok := ip.typesPkgs[ibp]
if !ok || p == nil {
continue
}
imps = append(imps, p)
}
tp.SetImports(imps)
ip.astFiles[bp] = files
ip.typesPkgs[bp] = tp
return tp, nil
}
func parsePackage(fset *token.FileSet, bp *build.Package) ([]*ast.File, error) {
files := make([]string, 0, len(bp.GoFiles)+len(bp.CgoFiles))
for _, f := range bp.GoFiles {
files = append(files, f)
}
for _, f := range bp.CgoFiles {
files = append(files, f)
}
res := make([]*ast.File, 0, len(files))
for _, f := range files {
var fname string
if bp.Name == "main" {
fname = f
} else {
fname = filepath.Join(bp.ImportPath, f)
}
path := filepath.Join(bp.Dir, f)
af, err := parseFile(fset, fname, path)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
res = append(res, af)
}
return res, nil
}
func parseFile(fset *token.FileSet, fname, path string) (*ast.File, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading: %w", err)
}
f, err := parser.ParseFile(fset, fname, b, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parsing: %w", err)
}
return f, nil
}
// getImportDirAbs finds abs path of the package pointed by the gvien importPath on the given dir.
func getImportDirAbs(importPath, dir string) (string, error) {
bp, err := build.Import(importPath, dir, build.FindOnly)
if err != nil {
return "", fmt.Errorf("finding %q on %q: %w", importPath, dir, err)
}
return bp.Dir, nil
}