github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/lintcmd/cmd.go (about)

     1  // Package lintcmd implements the frontend of an analysis runner.
     2  // It serves as the entry-point for the staticcheck command, and can also be used to implement custom linters that behave like staticcheck.
     3  package lintcmd
     4  
     5  import (
     6  	"bufio"
     7  	"encoding/gob"
     8  	"flag"
     9  	"fmt"
    10  	"go/token"
    11  	"io"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  	"reflect"
    16  	"runtime"
    17  	"runtime/pprof"
    18  	"runtime/trace"
    19  	"sort"
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/amarpal/go-tools/analysis/lint"
    25  	"github.com/amarpal/go-tools/config"
    26  	"github.com/amarpal/go-tools/go/loader"
    27  	"github.com/amarpal/go-tools/lintcmd/version"
    28  
    29  	"golang.org/x/tools/go/analysis"
    30  	"golang.org/x/tools/go/buildutil"
    31  )
    32  
    33  type buildConfig struct {
    34  	Name  string
    35  	Envs  []string
    36  	Flags []string
    37  }
    38  
    39  // Command represents a linter command line tool.
    40  type Command struct {
    41  	name           string
    42  	analyzers      map[string]*lint.Analyzer
    43  	version        string
    44  	machineVersion string
    45  
    46  	flags struct {
    47  		fs *flag.FlagSet
    48  
    49  		tags        string
    50  		tests       bool
    51  		showIgnored bool
    52  		formatter   string
    53  
    54  		// mutually exclusive mode flags
    55  		explain      string
    56  		printVersion bool
    57  		listChecks   bool
    58  		merge        bool
    59  
    60  		matrix bool
    61  
    62  		debugCpuprofile       string
    63  		debugMemprofile       string
    64  		debugVersion          bool
    65  		debugNoCompileErrors  bool
    66  		debugMeasureAnalyzers string
    67  		debugTrace            string
    68  
    69  		checks    list
    70  		fail      list
    71  		goVersion versionFlag
    72  	}
    73  }
    74  
    75  // NewCommand returns a new Command.
    76  func NewCommand(name string) *Command {
    77  	cmd := &Command{
    78  		name:           name,
    79  		analyzers:      map[string]*lint.Analyzer{},
    80  		version:        "devel",
    81  		machineVersion: "devel",
    82  	}
    83  	cmd.initFlagSet(name)
    84  	return cmd
    85  }
    86  
    87  // SetVersion sets the command's version.
    88  // It is divided into a human part and a machine part.
    89  // For example, Staticcheck 2020.2.1 had the human version "2020.2.1" and the machine version "v0.1.1".
    90  // If you only use Semver, you can set both parts to the same value.
    91  //
    92  // Calling this method is optional. Both versions default to "devel", and we'll attempt to deduce more version information from the Go module.
    93  func (cmd *Command) SetVersion(human, machine string) {
    94  	cmd.version = human
    95  	cmd.machineVersion = machine
    96  }
    97  
    98  // FlagSet returns the command's flag set.
    99  // This can be used to add additional command line arguments.
   100  func (cmd *Command) FlagSet() *flag.FlagSet {
   101  	return cmd.flags.fs
   102  }
   103  
   104  // AddAnalyzers adds analyzers to the command.
   105  // These are lint.Analyzer analyzers, which wrap analysis.Analyzer analyzers, bundling them with structured documentation.
   106  //
   107  // To add analysis.Analyzer analyzers without providing structured documentation, use AddBareAnalyzers.
   108  func (cmd *Command) AddAnalyzers(as ...*lint.Analyzer) {
   109  	for _, a := range as {
   110  		cmd.analyzers[a.Analyzer.Name] = a
   111  	}
   112  }
   113  
   114  // AddBareAnalyzers adds bare analyzers to the command.
   115  func (cmd *Command) AddBareAnalyzers(as ...*analysis.Analyzer) {
   116  	for _, a := range as {
   117  		var title, text string
   118  		if idx := strings.Index(a.Doc, "\n\n"); idx > -1 {
   119  			title = a.Doc[:idx]
   120  			text = a.Doc[idx+2:]
   121  		}
   122  
   123  		doc := &lint.Documentation{
   124  			Title:    title,
   125  			Text:     text,
   126  			Severity: lint.SeverityWarning,
   127  		}
   128  
   129  		cmd.analyzers[a.Name] = &lint.Analyzer{
   130  			Doc:      doc,
   131  			Analyzer: a,
   132  		}
   133  	}
   134  }
   135  
   136  func (cmd *Command) initFlagSet(name string) {
   137  	flags := flag.NewFlagSet("", flag.ExitOnError)
   138  	cmd.flags.fs = flags
   139  	flags.Usage = usage(name, flags)
   140  
   141  	flags.StringVar(&cmd.flags.tags, "tags", "", "List of `build tags`")
   142  	flags.BoolVar(&cmd.flags.tests, "tests", true, "Include tests")
   143  	flags.BoolVar(&cmd.flags.printVersion, "version", false, "Print version and exit")
   144  	flags.BoolVar(&cmd.flags.showIgnored, "show-ignored", false, "Don't filter ignored diagnostics")
   145  	flags.StringVar(&cmd.flags.formatter, "f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')")
   146  	flags.StringVar(&cmd.flags.explain, "explain", "", "Print description of `check`")
   147  	flags.BoolVar(&cmd.flags.listChecks, "list-checks", false, "List all available checks")
   148  	flags.BoolVar(&cmd.flags.merge, "merge", false, "Merge results of multiple Staticcheck runs")
   149  	flags.BoolVar(&cmd.flags.matrix, "matrix", false, "Read a build config matrix from stdin")
   150  
   151  	flags.StringVar(&cmd.flags.debugCpuprofile, "debug.cpuprofile", "", "Write CPU profile to `file`")
   152  	flags.StringVar(&cmd.flags.debugMemprofile, "debug.memprofile", "", "Write memory profile to `file`")
   153  	flags.BoolVar(&cmd.flags.debugVersion, "debug.version", false, "Print detailed version information about this program")
   154  	flags.BoolVar(&cmd.flags.debugNoCompileErrors, "debug.no-compile-errors", false, "Don't print compile errors")
   155  	flags.StringVar(&cmd.flags.debugMeasureAnalyzers, "debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
   156  	flags.StringVar(&cmd.flags.debugTrace, "debug.trace", "", "Write trace to `file`")
   157  
   158  	cmd.flags.checks = list{"inherit"}
   159  	cmd.flags.fail = list{"all"}
   160  	cmd.flags.goVersion = versionFlag("module")
   161  	flags.Var(&cmd.flags.checks, "checks", "Comma-separated list of `checks` to enable.")
   162  	flags.Var(&cmd.flags.fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
   163  	flags.Var(&cmd.flags.goVersion, "go", "Target Go `version` in the format '1.x', or the literal 'module' to use the module's Go version")
   164  }
   165  
   166  type list []string
   167  
   168  func (list *list) String() string {
   169  	return `"` + strings.Join(*list, ",") + `"`
   170  }
   171  
   172  func (list *list) Set(s string) error {
   173  	if s == "" {
   174  		*list = nil
   175  		return nil
   176  	}
   177  
   178  	elems := strings.Split(s, ",")
   179  	for i, elem := range elems {
   180  		elems[i] = strings.TrimSpace(elem)
   181  	}
   182  	*list = elems
   183  	return nil
   184  }
   185  
   186  type versionFlag string
   187  
   188  func (v *versionFlag) String() string {
   189  	return fmt.Sprintf("%q", string(*v))
   190  }
   191  
   192  func (v *versionFlag) Set(s string) error {
   193  	if s == "module" {
   194  		*v = "module"
   195  	} else {
   196  		var vf lint.VersionFlag
   197  		if err := vf.Set(s); err != nil {
   198  			return err
   199  		}
   200  		*v = versionFlag(s)
   201  	}
   202  	return nil
   203  }
   204  
   205  // ParseFlags parses command line flags.
   206  // It must be called before calling Run.
   207  // After calling ParseFlags, the values of flags can be accessed.
   208  //
   209  // Example:
   210  //
   211  //	cmd.ParseFlags(os.Args[1:])
   212  func (cmd *Command) ParseFlags(args []string) {
   213  	cmd.flags.fs.Parse(args)
   214  }
   215  
   216  // diagnosticDescriptor represents the uniquiely identifying information of diagnostics.
   217  type diagnosticDescriptor struct {
   218  	Position token.Position
   219  	End      token.Position
   220  	Category string
   221  	Message  string
   222  }
   223  
   224  func (diag diagnostic) descriptor() diagnosticDescriptor {
   225  	return diagnosticDescriptor{
   226  		Position: diag.Position,
   227  		End:      diag.End,
   228  		Category: diag.Category,
   229  		Message:  diag.Message,
   230  	}
   231  }
   232  
   233  type run struct {
   234  	checkedFiles map[string]struct{}
   235  	diagnostics  map[diagnosticDescriptor]diagnostic
   236  }
   237  
   238  func runFromLintResult(res lintResult) run {
   239  	out := run{
   240  		checkedFiles: map[string]struct{}{},
   241  		diagnostics:  map[diagnosticDescriptor]diagnostic{},
   242  	}
   243  
   244  	for _, cf := range res.CheckedFiles {
   245  		out.checkedFiles[cf] = struct{}{}
   246  	}
   247  	for _, diag := range res.Diagnostics {
   248  		out.diagnostics[diag.descriptor()] = diag
   249  	}
   250  	return out
   251  }
   252  
   253  func decodeGob(br io.ByteReader) ([]run, error) {
   254  	var runs []run
   255  	for {
   256  		var res lintResult
   257  		if err := gob.NewDecoder(br.(io.Reader)).Decode(&res); err != nil {
   258  			if err == io.EOF {
   259  				break
   260  			} else {
   261  				return nil, err
   262  			}
   263  		}
   264  		runs = append(runs, runFromLintResult(res))
   265  	}
   266  	return runs, nil
   267  }
   268  
   269  // Run runs all registered analyzers and reports their findings.
   270  // It always calls os.Exit and does not return.
   271  func (cmd *Command) Run() {
   272  	// Set up profiling and tracing
   273  	if path := cmd.flags.debugCpuprofile; path != "" {
   274  		f, err := os.Create(path)
   275  		if err != nil {
   276  			log.Fatal(err)
   277  		}
   278  		pprof.StartCPUProfile(f)
   279  	}
   280  	if path := cmd.flags.debugTrace; path != "" {
   281  		f, err := os.Create(path)
   282  		if err != nil {
   283  			log.Fatal(err)
   284  		}
   285  		trace.Start(f)
   286  	}
   287  
   288  	// Update the default config's list of enabled checks
   289  	defaultChecks := []string{"all"}
   290  	for _, a := range cmd.analyzers {
   291  		if a.Doc.NonDefault {
   292  			defaultChecks = append(defaultChecks, "-"+a.Analyzer.Name)
   293  		}
   294  	}
   295  	config.DefaultConfig.Checks = defaultChecks
   296  
   297  	// Run the appropriate mode
   298  	var exit int
   299  	switch {
   300  	case cmd.flags.debugVersion:
   301  		exit = cmd.printDebugVersion()
   302  	case cmd.flags.listChecks:
   303  		exit = cmd.listChecks()
   304  	case cmd.flags.printVersion:
   305  		exit = cmd.printVersion()
   306  	case cmd.flags.explain != "":
   307  		exit = cmd.explain()
   308  	case cmd.flags.merge:
   309  		exit = cmd.merge()
   310  	default:
   311  		exit = cmd.lint()
   312  	}
   313  
   314  	// Stop profiling
   315  	if cmd.flags.debugCpuprofile != "" {
   316  		pprof.StopCPUProfile()
   317  	}
   318  	if path := cmd.flags.debugMemprofile; path != "" {
   319  		f, err := os.Create(path)
   320  		if err != nil {
   321  			panic(err)
   322  		}
   323  		runtime.GC()
   324  		pprof.WriteHeapProfile(f)
   325  	}
   326  	if cmd.flags.debugTrace != "" {
   327  		trace.Stop()
   328  	}
   329  
   330  	// Exit with appropriate status
   331  	os.Exit(exit)
   332  }
   333  
   334  func (cmd *Command) analyzersAsSlice() []*lint.Analyzer {
   335  	cs := make([]*lint.Analyzer, 0, len(cmd.analyzers))
   336  	for _, a := range cmd.analyzers {
   337  		cs = append(cs, a)
   338  	}
   339  	return cs
   340  }
   341  
   342  func (cmd *Command) printDebugVersion() int {
   343  	version.Verbose(cmd.version, cmd.machineVersion)
   344  	return 0
   345  }
   346  
   347  func (cmd *Command) listChecks() int {
   348  	cs := cmd.analyzersAsSlice()
   349  	sort.Slice(cs, func(i, j int) bool {
   350  		return cs[i].Analyzer.Name < cs[j].Analyzer.Name
   351  	})
   352  	for _, c := range cs {
   353  		var title string
   354  		if c.Doc != nil {
   355  			title = c.Doc.Title
   356  		}
   357  		fmt.Printf("%s %s\n", c.Analyzer.Name, title)
   358  	}
   359  	return 0
   360  }
   361  
   362  func (cmd *Command) printVersion() int {
   363  	version.Print(cmd.version, cmd.machineVersion)
   364  	return 0
   365  }
   366  
   367  func (cmd *Command) explain() int {
   368  	explain := cmd.flags.explain
   369  	check, ok := cmd.analyzers[explain]
   370  	if !ok {
   371  		fmt.Fprintln(os.Stderr, "Couldn't find check", explain)
   372  		return 1
   373  	}
   374  	if check.Analyzer.Doc == "" {
   375  		fmt.Fprintln(os.Stderr, explain, "has no documentation")
   376  		return 1
   377  	}
   378  	fmt.Println(check.Doc)
   379  	fmt.Println("Online documentation\n    https://staticcheck.dev/docs/checks#" + check.Analyzer.Name)
   380  	return 0
   381  }
   382  
   383  func (cmd *Command) merge() int {
   384  	var runs []run
   385  	if len(cmd.flags.fs.Args()) == 0 {
   386  		var err error
   387  		runs, err = decodeGob(bufio.NewReader(os.Stdin))
   388  		if err != nil {
   389  			fmt.Fprintln(os.Stderr, fmt.Errorf("couldn't parse stdin: %s", err))
   390  			return 1
   391  		}
   392  	} else {
   393  		for _, path := range cmd.flags.fs.Args() {
   394  			someRuns, err := func(path string) ([]run, error) {
   395  				f, err := os.Open(path)
   396  				if err != nil {
   397  					return nil, err
   398  				}
   399  				defer f.Close()
   400  				br := bufio.NewReader(f)
   401  				return decodeGob(br)
   402  			}(path)
   403  			if err != nil {
   404  				fmt.Fprintln(os.Stderr, fmt.Errorf("couldn't parse file %s: %s", path, err))
   405  				return 1
   406  			}
   407  			runs = append(runs, someRuns...)
   408  		}
   409  	}
   410  
   411  	relevantDiagnostics := mergeRuns(runs)
   412  	cs := cmd.analyzersAsSlice()
   413  	return cmd.printDiagnostics(cs, relevantDiagnostics)
   414  }
   415  
   416  func (cmd *Command) lint() int {
   417  	switch cmd.flags.formatter {
   418  	case "text", "stylish", "json", "sarif", "binary", "null":
   419  	default:
   420  		fmt.Fprintf(os.Stderr, "unsupported output format %q\n", cmd.flags.formatter)
   421  		return 2
   422  	}
   423  
   424  	var bconfs []buildConfig
   425  	if cmd.flags.matrix {
   426  		if cmd.flags.tags != "" {
   427  			fmt.Fprintln(os.Stderr, "cannot use -matrix and -tags together")
   428  			return 2
   429  		}
   430  
   431  		var err error
   432  		bconfs, err = parseBuildConfigs(os.Stdin)
   433  		if err != nil {
   434  			if perr, ok := err.(parseBuildConfigError); ok {
   435  				fmt.Fprintf(os.Stderr, "<stdin>:%d couldn't parse build matrix: %s\n", perr.line, perr.err)
   436  			} else {
   437  				fmt.Fprintln(os.Stderr, err)
   438  			}
   439  			return 2
   440  		}
   441  	} else {
   442  		bc := buildConfig{}
   443  		if cmd.flags.tags != "" {
   444  			// Validate that the tags argument is well-formed. go/packages
   445  			// doesn't detect malformed build flags and returns unhelpful
   446  			// errors.
   447  			tf := buildutil.TagsFlag{}
   448  			if err := tf.Set(cmd.flags.tags); err != nil {
   449  				fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", cmd.flags.tags, err))
   450  				return 1
   451  			}
   452  
   453  			bc.Flags = []string{"-tags", cmd.flags.tags}
   454  		}
   455  		bconfs = append(bconfs, bc)
   456  	}
   457  
   458  	var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
   459  	if path := cmd.flags.debugMeasureAnalyzers; path != "" {
   460  		f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
   461  		if err != nil {
   462  			log.Fatal(err)
   463  		}
   464  
   465  		mu := &sync.Mutex{}
   466  		measureAnalyzers = func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) {
   467  			mu.Lock()
   468  			defer mu.Unlock()
   469  			// FIXME(dh): print pkg.ID
   470  			if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg, d.Nanoseconds()); err != nil {
   471  				log.Println("error writing analysis measurements:", err)
   472  			}
   473  		}
   474  	}
   475  
   476  	var runs []run
   477  	cs := cmd.analyzersAsSlice()
   478  	opts := options{
   479  		analyzers: cs,
   480  		patterns:  cmd.flags.fs.Args(),
   481  		lintTests: cmd.flags.tests,
   482  		goVersion: string(cmd.flags.goVersion),
   483  		config: config.Config{
   484  			Checks: cmd.flags.checks,
   485  		},
   486  		printAnalyzerMeasurement: measureAnalyzers,
   487  	}
   488  	l, err := newLinter(opts)
   489  	if err != nil {
   490  		fmt.Fprintln(os.Stderr, err)
   491  		return 1
   492  	}
   493  	for _, bconf := range bconfs {
   494  		res, err := l.run(bconf)
   495  		if err != nil {
   496  			fmt.Fprintln(os.Stderr, err)
   497  			return 1
   498  		}
   499  
   500  		for _, w := range res.Warnings {
   501  			fmt.Fprintln(os.Stderr, "warning:", w)
   502  		}
   503  
   504  		cwd, err := os.Getwd()
   505  		if err != nil {
   506  			cwd = ""
   507  		}
   508  		relPath := func(s string) string {
   509  			if cwd == "" {
   510  				return filepath.ToSlash(s)
   511  			}
   512  			out, err := filepath.Rel(cwd, s)
   513  			if err != nil {
   514  				return filepath.ToSlash(s)
   515  			}
   516  			return filepath.ToSlash(out)
   517  		}
   518  
   519  		if cmd.flags.formatter == "binary" {
   520  			for i, s := range res.CheckedFiles {
   521  				res.CheckedFiles[i] = relPath(s)
   522  			}
   523  			for i := range res.Diagnostics {
   524  				// We turn all paths into relative, /-separated paths. This is to make -merge work correctly when
   525  				// merging runs from different OSs, with different absolute paths.
   526  				//
   527  				// We zero out Offset, because checkouts of code on different OSs may have different kinds of
   528  				// newlines and thus different offsets. We don't ever make use of the Offset, anyway. Line and
   529  				// column numbers are precomputed.
   530  
   531  				d := &res.Diagnostics[i]
   532  				d.Position.Filename = relPath(d.Position.Filename)
   533  				d.Position.Offset = 0
   534  				d.End.Filename = relPath(d.End.Filename)
   535  				d.End.Offset = 0
   536  				for j := range d.Related {
   537  					r := &d.Related[j]
   538  					r.Position.Filename = relPath(r.Position.Filename)
   539  					r.Position.Offset = 0
   540  					r.End.Filename = relPath(r.End.Filename)
   541  					r.End.Offset = 0
   542  				}
   543  			}
   544  			err := gob.NewEncoder(os.Stdout).Encode(res)
   545  			if err != nil {
   546  				fmt.Fprintf(os.Stderr, "failed writing output: %s\n", err)
   547  				return 2
   548  			}
   549  		} else {
   550  			runs = append(runs, runFromLintResult(res))
   551  		}
   552  	}
   553  
   554  	l.cache.Trim()
   555  
   556  	if cmd.flags.formatter != "binary" {
   557  		diags := mergeRuns(runs)
   558  		return cmd.printDiagnostics(cs, diags)
   559  	}
   560  	return 0
   561  }
   562  
   563  func mergeRuns(runs []run) []diagnostic {
   564  	var relevantDiagnostics []diagnostic
   565  	for _, r := range runs {
   566  		for _, diag := range r.diagnostics {
   567  			switch diag.MergeIf {
   568  			case lint.MergeIfAny:
   569  				relevantDiagnostics = append(relevantDiagnostics, diag)
   570  			case lint.MergeIfAll:
   571  				doPrint := true
   572  				for _, r := range runs {
   573  					if _, ok := r.checkedFiles[diag.Position.Filename]; ok {
   574  						if _, ok := r.diagnostics[diag.descriptor()]; !ok {
   575  							doPrint = false
   576  						}
   577  					}
   578  				}
   579  				if doPrint {
   580  					relevantDiagnostics = append(relevantDiagnostics, diag)
   581  				}
   582  			}
   583  		}
   584  	}
   585  	return relevantDiagnostics
   586  }
   587  
   588  // printDiagnostics prints the diagnostics and exits the process.
   589  func (cmd *Command) printDiagnostics(cs []*lint.Analyzer, diagnostics []diagnostic) int {
   590  	if len(diagnostics) > 1 {
   591  		sort.Slice(diagnostics, func(i, j int) bool {
   592  			di := diagnostics[i]
   593  			dj := diagnostics[j]
   594  			pi := di.Position
   595  			pj := dj.Position
   596  
   597  			if pi.Filename != pj.Filename {
   598  				return pi.Filename < pj.Filename
   599  			}
   600  			if pi.Line != pj.Line {
   601  				return pi.Line < pj.Line
   602  			}
   603  			if pi.Column != pj.Column {
   604  				return pi.Column < pj.Column
   605  			}
   606  			if di.Message != dj.Message {
   607  				return di.Message < dj.Message
   608  			}
   609  			if di.BuildName != dj.BuildName {
   610  				return di.BuildName < dj.BuildName
   611  			}
   612  			return di.Category < dj.Category
   613  		})
   614  
   615  		filtered := []diagnostic{
   616  			diagnostics[0],
   617  		}
   618  		builds := []map[string]struct{}{
   619  			{diagnostics[0].BuildName: {}},
   620  		}
   621  		for _, diag := range diagnostics[1:] {
   622  			// We may encounter duplicate diagnostics because one file
   623  			// can be part of many packages, and because multiple
   624  			// build configurations may check the same files.
   625  			if !filtered[len(filtered)-1].equal(diag) {
   626  				if filtered[len(filtered)-1].descriptor() == diag.descriptor() {
   627  					// Diagnostics only differ in build name, track new name
   628  					builds[len(filtered)-1][diag.BuildName] = struct{}{}
   629  				} else {
   630  					filtered = append(filtered, diag)
   631  					builds = append(builds, map[string]struct{}{})
   632  					builds[len(filtered)-1][diag.BuildName] = struct{}{}
   633  				}
   634  			}
   635  		}
   636  
   637  		var names []string
   638  		for i := range filtered {
   639  			names = names[:0]
   640  			for k := range builds[i] {
   641  				names = append(names, k)
   642  			}
   643  			sort.Strings(names)
   644  			filtered[i].BuildName = strings.Join(names, ",")
   645  		}
   646  		diagnostics = filtered
   647  	}
   648  
   649  	var f formatter
   650  	switch cmd.flags.formatter {
   651  	case "text":
   652  		f = textFormatter{W: os.Stdout}
   653  	case "stylish":
   654  		f = &stylishFormatter{W: os.Stdout}
   655  	case "json":
   656  		f = jsonFormatter{W: os.Stdout}
   657  	case "sarif":
   658  		f = &sarifFormatter{
   659  			driverName:    cmd.name,
   660  			driverVersion: cmd.version,
   661  		}
   662  		if cmd.name == "staticcheck" {
   663  			f.(*sarifFormatter).driverName = "Staticcheck"
   664  			f.(*sarifFormatter).driverWebsite = "https://staticcheck.dev"
   665  		}
   666  	case "binary":
   667  		fmt.Fprintln(os.Stderr, "'-f binary' not supported in this context")
   668  		return 2
   669  	case "null":
   670  		f = nullFormatter{}
   671  	default:
   672  		fmt.Fprintf(os.Stderr, "unsupported output format %q\n", cmd.flags.formatter)
   673  		return 2
   674  	}
   675  
   676  	fail := cmd.flags.fail
   677  	analyzerNames := make([]string, len(cs))
   678  	for i, a := range cs {
   679  		analyzerNames[i] = a.Analyzer.Name
   680  	}
   681  	shouldExit := filterAnalyzerNames(analyzerNames, fail)
   682  	shouldExit["staticcheck"] = true
   683  	shouldExit["compile"] = true
   684  
   685  	var (
   686  		numErrors   int
   687  		numWarnings int
   688  		numIgnored  int
   689  	)
   690  	notIgnored := make([]diagnostic, 0, len(diagnostics))
   691  	for _, diag := range diagnostics {
   692  		if diag.Category == "compile" && cmd.flags.debugNoCompileErrors {
   693  			continue
   694  		}
   695  		if diag.Severity == severityIgnored && !cmd.flags.showIgnored {
   696  			numIgnored++
   697  			continue
   698  		}
   699  		if shouldExit[diag.Category] {
   700  			numErrors++
   701  		} else {
   702  			diag.Severity = severityWarning
   703  			numWarnings++
   704  		}
   705  		notIgnored = append(notIgnored, diag)
   706  	}
   707  
   708  	f.Format(cs, notIgnored)
   709  	if f, ok := f.(statter); ok {
   710  		f.Stats(len(diagnostics), numErrors, numWarnings, numIgnored)
   711  	}
   712  
   713  	if numErrors > 0 {
   714  		if _, ok := f.(*sarifFormatter); ok {
   715  			// When emitting SARIF, finding errors is considered success.
   716  			return 0
   717  		} else {
   718  			return 1
   719  		}
   720  	}
   721  	return 0
   722  }
   723  
   724  func usage(name string, fs *flag.FlagSet) func() {
   725  	return func() {
   726  		fmt.Fprintf(os.Stderr, "Usage: %s [flags] [packages]\n", name)
   727  
   728  		fmt.Fprintln(os.Stderr)
   729  		fmt.Fprintln(os.Stderr, "Flags:")
   730  		printDefaults(fs)
   731  
   732  		fmt.Fprintln(os.Stderr)
   733  		fmt.Fprintln(os.Stderr, "For help about specifying packages, see 'go help packages'")
   734  	}
   735  }
   736  
   737  // isZeroValue determines whether the string represents the zero
   738  // value for a flag.
   739  //
   740  // this function has been copied from the Go standard library's 'flag' package.
   741  func isZeroValue(f *flag.Flag, value string) bool {
   742  	// Build a zero value of the flag's Value type, and see if the
   743  	// result of calling its String method equals the value passed in.
   744  	// This works unless the Value type is itself an interface type.
   745  	typ := reflect.TypeOf(f.Value)
   746  	var z reflect.Value
   747  	if typ.Kind() == reflect.Ptr {
   748  		z = reflect.New(typ.Elem())
   749  	} else {
   750  		z = reflect.Zero(typ)
   751  	}
   752  	return value == z.Interface().(flag.Value).String()
   753  }
   754  
   755  // this function has been copied from the Go standard library's 'flag' package and modified to skip debug flags.
   756  func printDefaults(fs *flag.FlagSet) {
   757  	fs.VisitAll(func(f *flag.Flag) {
   758  		// Don't print debug flags
   759  		if strings.HasPrefix(f.Name, "debug.") {
   760  			return
   761  		}
   762  
   763  		var b strings.Builder
   764  		fmt.Fprintf(&b, "  -%s", f.Name) // Two spaces before -; see next two comments.
   765  		name, usage := flag.UnquoteUsage(f)
   766  		if len(name) > 0 {
   767  			b.WriteString(" ")
   768  			b.WriteString(name)
   769  		}
   770  		// Boolean flags of one ASCII letter are so common we
   771  		// treat them specially, putting their usage on the same line.
   772  		if b.Len() <= 4 { // space, space, '-', 'x'.
   773  			b.WriteString("\t")
   774  		} else {
   775  			// Four spaces before the tab triggers good alignment
   776  			// for both 4- and 8-space tab stops.
   777  			b.WriteString("\n    \t")
   778  		}
   779  		b.WriteString(strings.ReplaceAll(usage, "\n", "\n    \t"))
   780  
   781  		if !isZeroValue(f, f.DefValue) {
   782  			if T := reflect.TypeOf(f.Value); T.Name() == "*stringValue" && T.PkgPath() == "flag" {
   783  				// put quotes on the value
   784  				fmt.Fprintf(&b, " (default %q)", f.DefValue)
   785  			} else {
   786  				fmt.Fprintf(&b, " (default %v)", f.DefValue)
   787  			}
   788  		}
   789  		fmt.Fprint(fs.Output(), b.String(), "\n")
   790  	})
   791  }