src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

cmd.go (22152B)


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