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 }