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.
21package 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
29import (
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	"log"
42	"os"
43	"path/filepath"
44	"reflect"
45	"sort"
46	"strings"
47	"sync"
48	"time"
49
50	"golang.org/x/tools/go/analysis"
51	"golang.org/x/tools/go/analysis/internal/analysisflags"
52	"golang.org/x/tools/internal/analysisinternal"
53	"golang.org/x/tools/internal/facts"
54	"golang.org/x/tools/internal/versions"
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".
60type Config struct {
61	ID                        string // e.g. "fmt [fmt.test]"
62	Compiler                  string // gc or gccgo, provided to MakeImporter
63	Dir                       string // (unused)
64	ImportPath                string // package path
65	GoVersion                 string // minimum required Go version, such as "go1.21.0"
66	GoFiles                   []string
67	NonGoFiles                []string
68	IgnoredFiles              []string
69	ImportMap                 map[string]string // maps import path to package path
70	PackageFile               map[string]string // maps package path to file of type information
71	Standard                  map[string]bool   // package belongs to standard library
72	PackageVetx               map[string]string // maps package path to file of fact information
73	VetxOnly                  bool              // run analysis only for facts, not diagnostics
74	VetxOutput                string            // where to write file of fact information
75	SucceedOnTypecheckFailure bool
76}
77
78// Main is the main function of a vet-like analysis tool that must be
79// invoked by a build system to analyze a single package.
80//
81// The protocol required by 'go vet -vettool=...' is that the tool must support:
82//
83//	-flags          describe flags in JSON
84//	-V=full         describe executable for build caching
85//	foo.cfg         perform separate modular analyze on the single
86//	                unit described by a JSON config file foo.cfg.
87func Main(analyzers ...*analysis.Analyzer) {
88	progname := filepath.Base(os.Args[0])
89	log.SetFlags(0)
90	log.SetPrefix(progname + ": ")
91
92	if err := analysis.Validate(analyzers); err != nil {
93		log.Fatal(err)
94	}
95
96	flag.Usage = func() {
97		fmt.Fprintf(os.Stderr, `%[1]s is a tool for static analysis of Go programs.
98
99Usage of %[1]s:
100	%.16[1]s unit.cfg	# execute analysis specified by config file
101	%.16[1]s help    	# general help, including listing analyzers and flags
102	%.16[1]s help name	# help on specific analyzer and its flags
103`, progname)
104		os.Exit(1)
105	}
106
107	analyzers = analysisflags.Parse(analyzers, true)
108
109	args := flag.Args()
110	if len(args) == 0 {
111		flag.Usage()
112	}
113	if args[0] == "help" {
114		analysisflags.Help(progname, analyzers, args[1:])
115		os.Exit(0)
116	}
117	if len(args) != 1 || !strings.HasSuffix(args[0], ".cfg") {
118		log.Fatalf(`invoking "go tool vet" directly is unsupported; use "go vet"`)
119	}
120	Run(args[0], analyzers)
121}
122
123// Run reads the *.cfg file, runs the analysis,
124// and calls os.Exit with an appropriate error code.
125// It assumes flags have already been set.
126func Run(configFile string, analyzers []*analysis.Analyzer) {
127	cfg, err := readConfig(configFile)
128	if err != nil {
129		log.Fatal(err)
130	}
131
132	fset := token.NewFileSet()
133	results, err := run(fset, cfg, analyzers)
134	if err != nil {
135		log.Fatal(err)
136	}
137
138	// In VetxOnly mode, the analysis is run only for facts.
139	if !cfg.VetxOnly {
140		if analysisflags.JSON {
141			// JSON output
142			tree := make(analysisflags.JSONTree)
143			for _, res := range results {
144				tree.Add(fset, cfg.ID, res.a.Name, res.diagnostics, res.err)
145			}
146			tree.Print()
147		} else {
148			// plain text
149			exit := 0
150			for _, res := range results {
151				if res.err != nil {
152					log.Println(res.err)
153					exit = 1
154				}
155			}
156			for _, res := range results {
157				for _, diag := range res.diagnostics {
158					analysisflags.PrintPlain(fset, diag)
159					exit = 1
160				}
161			}
162			os.Exit(exit)
163		}
164	}
165
166	os.Exit(0)
167}
168
169func readConfig(filename string) (*Config, error) {
170	data, err := os.ReadFile(filename)
171	if err != nil {
172		return nil, err
173	}
174	cfg := new(Config)
175	if err := json.Unmarshal(data, cfg); err != nil {
176		return nil, fmt.Errorf("cannot decode JSON config file %s: %v", filename, err)
177	}
178	if len(cfg.GoFiles) == 0 {
179		// The go command disallows packages with no files.
180		// The only exception is unsafe, but the go command
181		// doesn't call vet on it.
182		return nil, fmt.Errorf("package has no files: %s", cfg.ImportPath)
183	}
184	return cfg, nil
185}
186
187type factImporter = func(pkgPath string) ([]byte, error)
188
189// These four hook variables are a proof of concept of a future
190// parameterization of a unitchecker API that allows the client to
191// determine how and where facts and types are produced and consumed.
192// (Note that the eventual API will likely be quite different.)
193//
194// The defaults honor a Config in a manner compatible with 'go vet'.
195var (
196	makeTypesImporter = func(cfg *Config, fset *token.FileSet) types.Importer {
197		compilerImporter := importer.ForCompiler(fset, cfg.Compiler, func(path string) (io.ReadCloser, error) {
198			// path is a resolved package path, not an import path.
199			file, ok := cfg.PackageFile[path]
200			if !ok {
201				if cfg.Compiler == "gccgo" && cfg.Standard[path] {
202					return nil, nil // fall back to default gccgo lookup
203				}
204				return nil, fmt.Errorf("no package file for %q", path)
205			}
206			return os.Open(file)
207		})
208		return importerFunc(func(importPath string) (*types.Package, error) {
209			path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc
210			if !ok {
211				return nil, fmt.Errorf("can't resolve import %q", path)
212			}
213			return compilerImporter.Import(path)
214		})
215	}
216
217	exportTypes = func(*Config, *token.FileSet, *types.Package) error {
218		// By default this is a no-op, because "go vet"
219		// makes the compiler produce type information.
220		return nil
221	}
222
223	makeFactImporter = func(cfg *Config) factImporter {
224		return func(pkgPath string) ([]byte, error) {
225			if vetx, ok := cfg.PackageVetx[pkgPath]; ok {
226				return os.ReadFile(vetx)
227			}
228			return nil, nil // no .vetx file, no facts
229		}
230	}
231
232	exportFacts = func(cfg *Config, data []byte) error {
233		return os.WriteFile(cfg.VetxOutput, data, 0666)
234	}
235)
236
237func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]result, error) {
238	// Load, parse, typecheck.
239	var files []*ast.File
240	for _, name := range cfg.GoFiles {
241		f, err := parser.ParseFile(fset, name, nil, parser.ParseComments)
242		if err != nil {
243			if cfg.SucceedOnTypecheckFailure {
244				// Silently succeed; let the compiler
245				// report parse errors.
246				err = nil
247			}
248			return nil, err
249		}
250		files = append(files, f)
251	}
252	tc := &types.Config{
253		Importer:  makeTypesImporter(cfg, fset),
254		Sizes:     types.SizesFor("gc", build.Default.GOARCH), // TODO(adonovan): use cfg.Compiler
255		GoVersion: cfg.GoVersion,
256	}
257	info := &types.Info{
258		Types:      make(map[ast.Expr]types.TypeAndValue),
259		Defs:       make(map[*ast.Ident]types.Object),
260		Uses:       make(map[*ast.Ident]types.Object),
261		Implicits:  make(map[ast.Node]types.Object),
262		Instances:  make(map[*ast.Ident]types.Instance),
263		Scopes:     make(map[ast.Node]*types.Scope),
264		Selections: make(map[*ast.SelectorExpr]*types.Selection),
265	}
266	versions.InitFileVersions(info)
267
268	pkg, err := tc.Check(cfg.ImportPath, fset, files, info)
269	if err != nil {
270		if cfg.SucceedOnTypecheckFailure {
271			// Silently succeed; let the compiler
272			// report type errors.
273			err = nil
274		}
275		return nil, err
276	}
277
278	// Register fact types with gob.
279	// In VetxOnly mode, analyzers are only for their facts,
280	// so we can skip any analysis that neither produces facts
281	// nor depends on any analysis that produces facts.
282	//
283	// TODO(adonovan): fix: the command (and logic!) here are backwards.
284	// It should say "...nor is required by any...". (Issue 443099)
285	//
286	// Also build a map to hold working state and result.
287	type action struct {
288		once        sync.Once
289		result      interface{}
290		err         error
291		usesFacts   bool // (transitively uses)
292		diagnostics []analysis.Diagnostic
293	}
294	actions := make(map[*analysis.Analyzer]*action)
295	var registerFacts func(a *analysis.Analyzer) bool
296	registerFacts = func(a *analysis.Analyzer) bool {
297		act, ok := actions[a]
298		if !ok {
299			act = new(action)
300			var usesFacts bool
301			for _, f := range a.FactTypes {
302				usesFacts = true
303				gob.Register(f)
304			}
305			for _, req := range a.Requires {
306				if registerFacts(req) {
307					usesFacts = true
308				}
309			}
310			act.usesFacts = usesFacts
311			actions[a] = act
312		}
313		return act.usesFacts
314	}
315	var filtered []*analysis.Analyzer
316	for _, a := range analyzers {
317		if registerFacts(a) || !cfg.VetxOnly {
318			filtered = append(filtered, a)
319		}
320	}
321	analyzers = filtered
322
323	// Read facts from imported packages.
324	facts, err := facts.NewDecoder(pkg).Decode(makeFactImporter(cfg))
325	if err != nil {
326		return nil, err
327	}
328
329	// In parallel, execute the DAG of analyzers.
330	var exec func(a *analysis.Analyzer) *action
331	var execAll func(analyzers []*analysis.Analyzer)
332	exec = func(a *analysis.Analyzer) *action {
333		act := actions[a]
334		act.once.Do(func() {
335			execAll(a.Requires) // prefetch dependencies in parallel
336
337			// The inputs to this analysis are the
338			// results of its prerequisites.
339			inputs := make(map[*analysis.Analyzer]interface{})
340			var failed []string
341			for _, req := range a.Requires {
342				reqact := exec(req)
343				if reqact.err != nil {
344					failed = append(failed, req.String())
345					continue
346				}
347				inputs[req] = reqact.result
348			}
349
350			// Report an error if any dependency failed.
351			if failed != nil {
352				sort.Strings(failed)
353				act.err = fmt.Errorf("failed prerequisites: %s", strings.Join(failed, ", "))
354				return
355			}
356
357			factFilter := make(map[reflect.Type]bool)
358			for _, f := range a.FactTypes {
359				factFilter[reflect.TypeOf(f)] = true
360			}
361
362			pass := &analysis.Pass{
363				Analyzer:          a,
364				Fset:              fset,
365				Files:             files,
366				OtherFiles:        cfg.NonGoFiles,
367				IgnoredFiles:      cfg.IgnoredFiles,
368				Pkg:               pkg,
369				TypesInfo:         info,
370				TypesSizes:        tc.Sizes,
371				TypeErrors:        nil, // unitchecker doesn't RunDespiteErrors
372				ResultOf:          inputs,
373				Report:            func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
374				ImportObjectFact:  facts.ImportObjectFact,
375				ExportObjectFact:  facts.ExportObjectFact,
376				AllObjectFacts:    func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) },
377				ImportPackageFact: facts.ImportPackageFact,
378				ExportPackageFact: facts.ExportPackageFact,
379				AllPackageFacts:   func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
380			}
381			pass.ReadFile = analysisinternal.MakeReadFile(pass)
382
383			t0 := time.Now()
384			act.result, act.err = a.Run(pass)
385
386			if act.err == nil { // resolve URLs on diagnostics.
387				for i := range act.diagnostics {
388					if url, uerr := analysisflags.ResolveURL(a, act.diagnostics[i]); uerr == nil {
389						act.diagnostics[i].URL = url
390					} else {
391						act.err = uerr // keep the last error
392					}
393				}
394			}
395			if false {
396				log.Printf("analysis %s = %s", pass, time.Since(t0))
397			}
398		})
399		return act
400	}
401	execAll = func(analyzers []*analysis.Analyzer) {
402		var wg sync.WaitGroup
403		for _, a := range analyzers {
404			wg.Add(1)
405			go func(a *analysis.Analyzer) {
406				_ = exec(a)
407				wg.Done()
408			}(a)
409		}
410		wg.Wait()
411	}
412
413	execAll(analyzers)
414
415	// Return diagnostics and errors from root analyzers.
416	results := make([]result, len(analyzers))
417	for i, a := range analyzers {
418		act := actions[a]
419		results[i].a = a
420		results[i].err = act.err
421		results[i].diagnostics = act.diagnostics
422	}
423
424	data := facts.Encode()
425	if err := exportFacts(cfg, data); err != nil {
426		return nil, fmt.Errorf("failed to export analysis facts: %v", err)
427	}
428	if err := exportTypes(cfg, fset, pkg); err != nil {
429		return nil, fmt.Errorf("failed to export type information: %v", err)
430	}
431
432	return results, nil
433}
434
435type result struct {
436	a           *analysis.Analyzer
437	diagnostics []analysis.Diagnostic
438	err         error
439}
440
441type importerFunc func(path string) (*types.Package, error)
442
443func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
444