| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package internal/checker defines various implementation helpers for |
| // the singlechecker and multichecker packages, which provide the |
| // complete main function for an analysis driver executable |
| // based on go/packages. |
| // |
| // (Note: it is not used by the public 'checker' package, since the |
| // latter provides a set of pure functions for use as building blocks.) |
| package checker |
| |
| // TODO(adonovan): publish the JSON schema in go/analysis or analysisjson. |
| |
| import ( |
| "flag" |
| "fmt" |
| "io" |
| |
| "log" |
| "os" |
| "runtime" |
| "runtime/pprof" |
| "runtime/trace" |
| "sort" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/checker" |
| "golang.org/x/tools/go/analysis/internal" |
| "golang.org/x/tools/go/analysis/internal/analysisflags" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/analysis/driverutil" |
| ) |
| |
| var ( |
| // Debug is a set of single-letter flags: |
| // |
| // f show [f]acts as they are created |
| // p disable [p]arallel execution of analyzers |
| // s do additional [s]anity checks on fact types and serialization |
| // t show [t]iming info (NB: use 'p' flag to avoid GC/scheduler noise) |
| // v show [v]erbose logging |
| // |
| Debug = "" |
| |
| // Log files for optional performance tracing. |
| CPUProfile, MemProfile, Trace string |
| |
| // IncludeTests indicates whether test files should be analyzed too. |
| IncludeTests = true |
| ) |
| |
| // RegisterFlags registers command-line flags used by the analysis driver. |
| func RegisterFlags() { |
| // When adding flags here, remember to update |
| // the list of suppressed flags in analysisflags. |
| |
| flag.StringVar(&Debug, "debug", Debug, `debug flags, any subset of "fpstv"`) |
| |
| flag.StringVar(&CPUProfile, "cpuprofile", "", "write CPU profile to this file") |
| flag.StringVar(&MemProfile, "memprofile", "", "write memory profile to this file") |
| flag.StringVar(&Trace, "trace", "", "write trace log to this file") |
| flag.BoolVar(&IncludeTests, "test", IncludeTests, "indicates whether test files should be analyzed, too") |
| } |
| |
| // Run loads the packages specified by args using go/packages, |
| // then applies the specified analyzers to them. |
| // Analysis flags must already have been set. |
| // Analyzers must be valid according to [analysis.Validate]. |
| // It provides most of the logic for the main functions of both the |
| // singlechecker and the multi-analysis commands. |
| // It returns the appropriate exit code. |
| // |
| // TODO(adonovan): tests should not call this function directly. |
| // Fiddling with global variables (flags such as [analysisflags.Fix]) |
| // is error-prone and hostile to parallelism. Instead, use unit tests |
| // of the actual units (e.g. checker.Analyze) and integration tests |
| // (e.g. TestScript) of whole executables. |
| func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { |
| // Instead of returning a code directly, |
| // call this function to monotonically increase the exit code. |
| // This allows us to keep going in the face of some errors |
| // without having to remember what code to return. |
| // |
| // TODO(adonovan): interpreting exit codes is like reading tea-leaves. |
| // Instead of wasting effort trying to encode a multidimensional result |
| // into 7 bits we should just emit structured JSON output, and |
| // an exit code of 0 or 1 for success or failure. |
| exitAtLeast := func(code int) { |
| exitcode = max(code, exitcode) |
| } |
| |
| // Since analysisflags is linked in (for {single,multi}checker), |
| // the -v flag is registered for complex legacy reasons |
| // related to cmd/vet CLI. |
| // Treat it as an undocumented alias for -debug=v. |
| if v := flag.CommandLine.Lookup("v"); v != nil && |
| v.Value.(flag.Getter).Get() == true && |
| !strings.Contains(Debug, "v") { |
| Debug += "v" |
| } |
| |
| if CPUProfile != "" { |
| f, err := os.Create(CPUProfile) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if err := pprof.StartCPUProfile(f); err != nil { |
| log.Fatal(err) |
| } |
| // NB: profile won't be written in case of error. |
| defer pprof.StopCPUProfile() |
| } |
| |
| if Trace != "" { |
| f, err := os.Create(Trace) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if err := trace.Start(f); err != nil { |
| log.Fatal(err) |
| } |
| // NB: trace log won't be written in case of error. |
| defer func() { |
| trace.Stop() |
| log.Printf("To view the trace, run:\n$ go tool trace view %s", Trace) |
| }() |
| } |
| |
| if MemProfile != "" { |
| f, err := os.Create(MemProfile) |
| if err != nil { |
| log.Fatal(err) |
| } |
| // NB: memprofile won't be written in case of error. |
| defer func() { |
| runtime.GC() // get up-to-date statistics |
| if err := pprof.WriteHeapProfile(f); err != nil { |
| log.Fatalf("Writing memory profile: %v", err) |
| } |
| f.Close() |
| }() |
| } |
| |
| // Load the packages. |
| if dbg('v') { |
| log.SetPrefix("") |
| log.SetFlags(log.Lmicroseconds) // display timing |
| log.Printf("load %s", args) |
| } |
| |
| // Optimization: if the selected analyzers don't produce/consume |
| // facts, we need source only for the initial packages. |
| allSyntax := needFacts(analyzers) |
| initial, err := load(args, allSyntax) |
| if err != nil { |
| log.Print(err) |
| exitAtLeast(1) |
| return |
| } |
| |
| // Print package and module errors regardless of RunDespiteErrors. |
| // Do not exit if there are errors, yet. |
| if n := packages.PrintErrors(initial); n > 0 { |
| exitAtLeast(1) |
| } |
| |
| var factLog io.Writer |
| if dbg('f') { |
| factLog = os.Stderr |
| } |
| |
| // Run the analysis. |
| opts := &checker.Options{ |
| SanityCheck: dbg('s'), |
| Sequential: dbg('p'), |
| FactLog: factLog, |
| } |
| if dbg('v') { |
| log.Printf("building graph of analysis passes") |
| } |
| graph, err := checker.Analyze(analyzers, initial, opts) |
| if err != nil { |
| log.Print(err) |
| exitAtLeast(1) |
| return |
| } |
| |
| // Don't print the diagnostics, |
| // but apply all fixes from the root actions. |
| if analysisflags.Fix { |
| fixActions := make([]driverutil.FixAction, len(graph.Roots)) |
| for i, act := range graph.Roots { |
| if pass := internal.ActionPass(act); pass != nil { |
| fixActions[i] = driverutil.FixAction{ |
| Name: act.String(), |
| Pkg: act.Package.Types, |
| Files: act.Package.Syntax, |
| FileSet: act.Package.Fset, |
| ReadFileFunc: pass.ReadFile, |
| Diagnostics: act.Diagnostics, |
| } |
| } |
| } |
| write := func(filename string, content []byte) error { |
| return os.WriteFile(filename, content, 0644) |
| } |
| if err := driverutil.ApplyFixes(fixActions, write, analysisflags.Diff, dbg('v')); err != nil { |
| // Fail when applying fixes failed. |
| log.Print(err) |
| exitAtLeast(1) |
| return |
| } |
| // Don't proceed to print text/JSON, |
| // and don't report an error |
| // just because there were diagnostics. |
| return |
| } |
| |
| // Print the results. If !RunDespiteErrors and there |
| // are errors in the packages, this will have 0 exit |
| // code. Otherwise, we prefer to return exit code |
| // indicating diagnostics. |
| exitAtLeast(printDiagnostics(graph)) |
| |
| return |
| } |
| |
| // printDiagnostics prints diagnostics in text or JSON form |
| // and returns the appropriate exit code. |
| func printDiagnostics(graph *checker.Graph) (exitcode int) { |
| // Keep consistent with analogous logic in |
| // processResults in ../../unitchecker/unitchecker.go. |
| |
| // Print the results. |
| // With -json, the exit code is always zero. |
| if analysisflags.JSON { |
| if err := graph.PrintJSON(os.Stdout); err != nil { |
| return 1 |
| } |
| } else { |
| if err := graph.PrintText(os.Stderr, analysisflags.Context); err != nil { |
| return 1 |
| } |
| |
| // Compute the exit code. |
| var numErrors, rootDiags int |
| for act := range graph.All() { |
| if act.Err != nil { |
| numErrors++ |
| } else if act.IsRoot { |
| rootDiags += len(act.Diagnostics) |
| } |
| } |
| |
| if numErrors > 0 { |
| exitcode = 1 // analysis failed, at least partially |
| } else if rootDiags > 0 { |
| exitcode = 3 // successfully produced diagnostics |
| } |
| } |
| |
| // Print timing info. |
| if dbg('t') { |
| if !dbg('p') { |
| log.Println("Warning: times are mostly GC/scheduler noise; use -debug=tp to disable parallelism") |
| } |
| |
| var list []*checker.Action |
| var total time.Duration |
| for act := range graph.All() { |
| list = append(list, act) |
| total += act.Duration |
| } |
| |
| // Print actions accounting for 90% of the total. |
| sort.Slice(list, func(i, j int) bool { |
| return list[i].Duration > list[j].Duration |
| }) |
| var sum time.Duration |
| for _, act := range list { |
| fmt.Fprintf(os.Stderr, "%s\t%s\n", act.Duration, act) |
| sum += act.Duration |
| if sum >= total*9/10 { |
| break |
| } |
| } |
| if total > sum { |
| fmt.Fprintf(os.Stderr, "%s\tall others\n", total-sum) |
| } |
| } |
| |
| return exitcode |
| } |
| |
| // load loads the initial packages. Returns only top-level loading |
| // errors. Does not consider errors in packages. |
| func load(patterns []string, allSyntax bool) ([]*packages.Package, error) { |
| mode := packages.LoadSyntax |
| if allSyntax { |
| mode = packages.LoadAllSyntax |
| } |
| mode |= packages.NeedModule |
| conf := packages.Config{ |
| Mode: mode, |
| // Ensure that child process inherits correct alias of PWD. |
| // (See discussion at Dir field of [exec.Command].) |
| // However, this currently breaks some tests. |
| // TODO(adonovan): Investigate. |
| // |
| // Dir: os.Getenv("PWD"), |
| Tests: IncludeTests, |
| } |
| initial, err := packages.Load(&conf, patterns...) |
| if err == nil && len(initial) == 0 { |
| err = fmt.Errorf("%s matched no packages", strings.Join(patterns, " ")) |
| } |
| return initial, err |
| } |
| |
| // needFacts reports whether any analysis required by the specified set |
| // needs facts. If so, we must load the entire program from source. |
| func needFacts(analyzers []*analysis.Analyzer) bool { |
| seen := make(map[*analysis.Analyzer]bool) |
| var q []*analysis.Analyzer // for BFS |
| q = append(q, analyzers...) |
| for len(q) > 0 { |
| a := q[0] |
| q = q[1:] |
| if !seen[a] { |
| seen[a] = true |
| if len(a.FactTypes) > 0 { |
| return true |
| } |
| q = append(q, a.Requires...) |
| } |
| } |
| return false |
| } |
| |
| func dbg(b byte) bool { return strings.IndexByte(Debug, b) >= 0 } |