cmd.go raw

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