1 | // Copyright 2018 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | // The unitchecker package defines the main function for an analysis |
6 | // driver that analyzes a single compilation unit during a build. |
7 | // It is invoked by a build system such as "go vet": |
8 | // |
9 | // $ go vet -vettool=$(which vet) |
10 | // |
11 | // It supports the following command-line protocol: |
12 | // |
13 | // -V=full describe executable (to the build tool) |
14 | // -flags describe flags (to the build tool) |
15 | // foo.cfg description of compilation unit (from the build tool) |
16 | // |
17 | // This package does not depend on go/packages. |
18 | // If you need a standalone tool, use multichecker, |
19 | // which supports this mode but can also load packages |
20 | // from source using go/packages. |
21 | package unitchecker |
22 | |
23 | // TODO(adonovan): |
24 | // - with gccgo, go build does not build standard library, |
25 | // so we will not get to analyze it. Yet we must in order |
26 | // to create base facts for, say, the fmt package for the |
27 | // printf checker. |
28 | |
29 | import ( |
30 | "encoding/gob" |
31 | "encoding/json" |
32 | "flag" |
33 | "fmt" |
34 | "go/ast" |
35 | "go/build" |
36 | "go/importer" |
37 | "go/parser" |
38 | "go/token" |
39 | "go/types" |
40 | "io" |
41 | "io/ioutil" |
42 | "log" |
43 | "os" |
44 | "path/filepath" |
45 | "reflect" |
46 | "sort" |
47 | "strings" |
48 | "sync" |
49 | "time" |
50 | |
51 | "golang.org/x/tools/go/analysis" |
52 | "golang.org/x/tools/go/analysis/internal/analysisflags" |
53 | "golang.org/x/tools/internal/facts" |
54 | "golang.org/x/tools/internal/typeparams" |
55 | ) |
56 | |
57 | // A Config describes a compilation unit to be analyzed. |
58 | // It is provided to the tool in a JSON-encoded file |
59 | // whose name ends with ".cfg". |
60 | type Config struct { |
61 | ID string // e.g. "fmt [fmt.test]" |
62 | Compiler string |
63 | Dir string |
64 | ImportPath string |
65 | GoFiles []string |
66 | NonGoFiles []string |
67 | IgnoredFiles []string |
68 | ImportMap map[string]string |
69 | PackageFile map[string]string |
70 | Standard map[string]bool |
71 | PackageVetx map[string]string |
72 | VetxOnly bool |
73 | VetxOutput string |
74 | SucceedOnTypecheckFailure bool |
75 | } |
76 | |
77 | // Main is the main function of a vet-like analysis tool that must be |
78 | // invoked by a build system to analyze a single package. |
79 | // |
80 | // The protocol required by 'go vet -vettool=...' is that the tool must support: |
81 | // |
82 | // -flags describe flags in JSON |
83 | // -V=full describe executable for build caching |
84 | // foo.cfg perform separate modular analyze on the single |
85 | // unit described by a JSON config file foo.cfg. |
86 | func Main(analyzers ...*analysis.Analyzer) { |
87 | progname := filepath.Base(os.Args[0]) |
88 | log.SetFlags(0) |
89 | log.SetPrefix(progname + ": ") |
90 | |
91 | if err := analysis.Validate(analyzers); err != nil { |
92 | log.Fatal(err) |
93 | } |
94 | |
95 | flag.Usage = func() { |
96 | fmt.Fprintf(os.Stderr, `%[1]s is a tool for static analysis of Go programs. |
97 | |
98 | Usage of %[1]s: |
99 | %.16[1]s unit.cfg # execute analysis specified by config file |
100 | %.16[1]s help # general help, including listing analyzers and flags |
101 | %.16[1]s help name # help on specific analyzer and its flags |
102 | `, progname) |
103 | os.Exit(1) |
104 | } |
105 | |
106 | analyzers = analysisflags.Parse(analyzers, true) |
107 | |
108 | args := flag.Args() |
109 | if len(args) == 0 { |
110 | flag.Usage() |
111 | } |
112 | if args[0] == "help" { |
113 | analysisflags.Help(progname, analyzers, args[1:]) |
114 | os.Exit(0) |
115 | } |
116 | if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") { |
117 | log.Fatalf(`invoking "go tool vet" directly is unsupported; use "go vet"`) |
118 | } |
119 | Run(args[0], analyzers) |
120 | } |
121 | |
122 | // Run reads the *.cfg file, runs the analysis, |
123 | // and calls os.Exit with an appropriate error code. |
124 | // It assumes flags have already been set. |
125 | func Run(configFile string, analyzers []*analysis.Analyzer) { |
126 | cfg, err := readConfig(configFile) |
127 | if err != nil { |
128 | log.Fatal(err) |
129 | } |
130 | |
131 | fset := token.NewFileSet() |
132 | results, err := run(fset, cfg, analyzers) |
133 | if err != nil { |
134 | log.Fatal(err) |
135 | } |
136 | |
137 | // In VetxOnly mode, the analysis is run only for facts. |
138 | if !cfg.VetxOnly { |
139 | if analysisflags.JSON { |
140 | // JSON output |
141 | tree := make(analysisflags.JSONTree) |
142 | for _, res := range results { |
143 | tree.Add(fset, cfg.ID, res.a.Name, res.diagnostics, res.err) |
144 | } |
145 | tree.Print() |
146 | } else { |
147 | // plain text |
148 | exit := 0 |
149 | for _, res := range results { |
150 | if res.err != nil { |
151 | log.Println(res.err) |
152 | exit = 1 |
153 | } |
154 | } |
155 | for _, res := range results { |
156 | for _, diag := range res.diagnostics { |
157 | analysisflags.PrintPlain(fset, diag) |
158 | exit = 1 |
159 | } |
160 | } |
161 | os.Exit(exit) |
162 | } |
163 | } |
164 | |
165 | os.Exit(0) |
166 | } |
167 | |
168 | func readConfig(filename string) (*Config, error) { |
169 | data, err := ioutil.ReadFile(filename) |
170 | if err != nil { |
171 | return nil, err |
172 | } |
173 | cfg := new(Config) |
174 | if err := json.Unmarshal(data, cfg); err != nil { |
175 | return nil, fmt.Errorf("cannot decode JSON config file %s: %v", filename, err) |
176 | } |
177 | if len(cfg.GoFiles) == 0 { |
178 | // The go command disallows packages with no files. |
179 | // The only exception is unsafe, but the go command |
180 | // doesn't call vet on it. |
181 | return nil, fmt.Errorf("package has no files: %s", cfg.ImportPath) |
182 | } |
183 | return cfg, nil |
184 | } |
185 | |
186 | var importerForCompiler = func(_ *token.FileSet, compiler string, lookup importer.Lookup) types.Importer { |
187 | // broken legacy implementation (https://golang.org/issue/28995) |
188 | return importer.For(compiler, lookup) |
189 | } |
190 | |
191 | func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]result, error) { |
192 | // Load, parse, typecheck. |
193 | var files []*ast.File |
194 | for _, name := range cfg.GoFiles { |
195 | f, err := parser.ParseFile(fset, name, nil, parser.ParseComments) |
196 | if err != nil { |
197 | if cfg.SucceedOnTypecheckFailure { |
198 | // Silently succeed; let the compiler |
199 | // report parse errors. |
200 | err = nil |
201 | } |
202 | return nil, err |
203 | } |
204 | files = append(files, f) |
205 | } |
206 | compilerImporter := importerForCompiler(fset, cfg.Compiler, func(path string) (io.ReadCloser, error) { |
207 | // path is a resolved package path, not an import path. |
208 | file, ok := cfg.PackageFile[path] |
209 | if !ok { |
210 | if cfg.Compiler == "gccgo" && cfg.Standard[path] { |
211 | return nil, nil // fall back to default gccgo lookup |
212 | } |
213 | return nil, fmt.Errorf("no package file for %q", path) |
214 | } |
215 | return os.Open(file) |
216 | }) |
217 | importer := importerFunc(func(importPath string) (*types.Package, error) { |
218 | path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc |
219 | if !ok { |
220 | return nil, fmt.Errorf("can't resolve import %q", path) |
221 | } |
222 | return compilerImporter.Import(path) |
223 | }) |
224 | tc := &types.Config{ |
225 | Importer: importer, |
226 | Sizes: types.SizesFor("gc", build.Default.GOARCH), // assume gccgo ≡ gc? |
227 | } |
228 | info := &types.Info{ |
229 | Types: make(map[ast.Expr]types.TypeAndValue), |
230 | Defs: make(map[*ast.Ident]types.Object), |
231 | Uses: make(map[*ast.Ident]types.Object), |
232 | Implicits: make(map[ast.Node]types.Object), |
233 | Scopes: make(map[ast.Node]*types.Scope), |
234 | Selections: make(map[*ast.SelectorExpr]*types.Selection), |
235 | } |
236 | typeparams.InitInstanceInfo(info) |
237 | |
238 | pkg, err := tc.Check(cfg.ImportPath, fset, files, info) |
239 | if err != nil { |
240 | if cfg.SucceedOnTypecheckFailure { |
241 | // Silently succeed; let the compiler |
242 | // report type errors. |
243 | err = nil |
244 | } |
245 | return nil, err |
246 | } |
247 | |
248 | // Register fact types with gob. |
249 | // In VetxOnly mode, analyzers are only for their facts, |
250 | // so we can skip any analysis that neither produces facts |
251 | // nor depends on any analysis that produces facts. |
252 | // |
253 | // TODO(adonovan): fix: the command (and logic!) here are backwards. |
254 | // It should say "...nor is required by any...". (Issue 443099) |
255 | // |
256 | // Also build a map to hold working state and result. |
257 | type action struct { |
258 | once sync.Once |
259 | result interface{} |
260 | err error |
261 | usesFacts bool // (transitively uses) |
262 | diagnostics []analysis.Diagnostic |
263 | } |
264 | actions := make(map[*analysis.Analyzer]*action) |
265 | var registerFacts func(a *analysis.Analyzer) bool |
266 | registerFacts = func(a *analysis.Analyzer) bool { |
267 | act, ok := actions[a] |
268 | if !ok { |
269 | act = new(action) |
270 | var usesFacts bool |
271 | for _, f := range a.FactTypes { |
272 | usesFacts = true |
273 | gob.Register(f) |
274 | } |
275 | for _, req := range a.Requires { |
276 | if registerFacts(req) { |
277 | usesFacts = true |
278 | } |
279 | } |
280 | act.usesFacts = usesFacts |
281 | actions[a] = act |
282 | } |
283 | return act.usesFacts |
284 | } |
285 | var filtered []*analysis.Analyzer |
286 | for _, a := range analyzers { |
287 | if registerFacts(a) || !cfg.VetxOnly { |
288 | filtered = append(filtered, a) |
289 | } |
290 | } |
291 | analyzers = filtered |
292 | |
293 | // Read facts from imported packages. |
294 | read := func(imp *types.Package) ([]byte, error) { |
295 | if vetx, ok := cfg.PackageVetx[imp.Path()]; ok { |
296 | return ioutil.ReadFile(vetx) |
297 | } |
298 | return nil, nil // no .vetx file, no facts |
299 | } |
300 | facts, err := facts.NewDecoder(pkg).Decode(read) |
301 | if err != nil { |
302 | return nil, err |
303 | } |
304 | |
305 | // In parallel, execute the DAG of analyzers. |
306 | var exec func(a *analysis.Analyzer) *action |
307 | var execAll func(analyzers []*analysis.Analyzer) |
308 | exec = func(a *analysis.Analyzer) *action { |
309 | act := actions[a] |
310 | act.once.Do(func() { |
311 | execAll(a.Requires) // prefetch dependencies in parallel |
312 | |
313 | // The inputs to this analysis are the |
314 | // results of its prerequisites. |
315 | inputs := make(map[*analysis.Analyzer]interface{}) |
316 | var failed []string |
317 | for _, req := range a.Requires { |
318 | reqact := exec(req) |
319 | if reqact.err != nil { |
320 | failed = append(failed, req.String()) |
321 | continue |
322 | } |
323 | inputs[req] = reqact.result |
324 | } |
325 | |
326 | // Report an error if any dependency failed. |
327 | if failed != nil { |
328 | sort.Strings(failed) |
329 | act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", ")) |
330 | return |
331 | } |
332 | |
333 | factFilter := make(map[reflect.Type]bool) |
334 | for _, f := range a.FactTypes { |
335 | factFilter[reflect.TypeOf(f)] = true |
336 | } |
337 | |
338 | pass := &analysis.Pass{ |
339 | Analyzer: a, |
340 | Fset: fset, |
341 | Files: files, |
342 | OtherFiles: cfg.NonGoFiles, |
343 | IgnoredFiles: cfg.IgnoredFiles, |
344 | Pkg: pkg, |
345 | TypesInfo: info, |
346 | TypesSizes: tc.Sizes, |
347 | TypeErrors: nil, // unitchecker doesn't RunDespiteErrors |
348 | ResultOf: inputs, |
349 | Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) }, |
350 | ImportObjectFact: facts.ImportObjectFact, |
351 | ExportObjectFact: facts.ExportObjectFact, |
352 | AllObjectFacts: func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) }, |
353 | ImportPackageFact: facts.ImportPackageFact, |
354 | ExportPackageFact: facts.ExportPackageFact, |
355 | AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) }, |
356 | } |
357 | |
358 | t0 := time.Now() |
359 | act.result, act.err = a.Run(pass) |
360 | if false { |
361 | log.Printf("analysis %s = %s", pass, time.Since(t0)) |
362 | } |
363 | }) |
364 | return act |
365 | } |
366 | execAll = func(analyzers []*analysis.Analyzer) { |
367 | var wg sync.WaitGroup |
368 | for _, a := range analyzers { |
369 | wg.Add(1) |
370 | go func(a *analysis.Analyzer) { |
371 | _ = exec(a) |
372 | wg.Done() |
373 | }(a) |
374 | } |
375 | wg.Wait() |
376 | } |
377 | |
378 | execAll(analyzers) |
379 | |
380 | // Return diagnostics and errors from root analyzers. |
381 | results := make([]result, len(analyzers)) |
382 | for i, a := range analyzers { |
383 | act := actions[a] |
384 | results[i].a = a |
385 | results[i].err = act.err |
386 | results[i].diagnostics = act.diagnostics |
387 | } |
388 | |
389 | data := facts.Encode() |
390 | if err := ioutil.WriteFile(cfg.VetxOutput, data, 0666); err != nil { |
391 | return nil, fmt.Errorf("failed to write analysis facts: %v", err) |
392 | } |
393 | |
394 | return results, nil |
395 | } |
396 | |
397 | type result struct { |
398 | a *analysis.Analyzer |
399 | diagnostics []analysis.Diagnostic |
400 | err error |
401 | } |
402 | |
403 | type importerFunc func(path string) (*types.Package, error) |
404 | |
405 | func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } |
406 |
Members