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/internal/cache"
29 "honnef.co/go/tools/lintcmd/runner"
30 "honnef.co/go/tools/lintcmd/version"
31 "honnef.co/go/tools/unused"
33 "golang.org/x/tools/go/analysis"
34 "golang.org/x/tools/go/buildutil"
35 "golang.org/x/tools/go/packages"
38 type ignore interface {
42 type lineIgnore struct {
50 func (li *lineIgnore) Match(p problem) bool {
52 if pos.Filename != li.File || pos.Line != li.Line {
55 for _, c := range li.Checks {
56 if m, _ := filepath.Match(c, p.Category); m {
64 func (li *lineIgnore) String() string {
65 matched := "not matched"
69 return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
72 type fileIgnore struct {
77 func (fi *fileIgnore) Match(p problem) bool {
78 if p.Position.Filename != fi.File {
81 for _, c := range fi.Checks {
82 if m, _ := filepath.Match(c, p.Category); m {
92 severityError severity = iota
97 func (s severity) String() string {
101 case severityWarning:
103 case severityIgnored:
106 return fmt.Sprintf("Severity(%d)", s)
110 // problem represents a problem in some source code.
111 type problem struct {
116 func (p problem) equal(o problem) bool {
117 return p.Position == o.Position &&
119 p.Message == o.Message &&
120 p.Category == o.Category &&
121 p.Severity == o.Severity
124 func (p *problem) String() string {
125 return fmt.Sprintf("%s (%s)", p.Message, p.Category)
128 // A linter lints Go source code.
130 Checkers []*analysis.Analyzer
132 Runner *runner.Runner
135 func failed(res runner.Result) []problem {
136 var problems []problem
138 for _, e := range res.Errors {
139 switch e := e.(type) {
142 if len(msg) != 0 && msg[0] == '\n' {
143 // TODO(dh): See https://github.com/golang/go/issues/32363
147 var posn token.Position
149 // Under certain conditions (malformed package
150 // declarations, multiple packages in the same
151 // directory), go list emits an error on stderr
152 // instead of JSON. Those errors do not have
153 // associated position information in
154 // go/packages.Error, even though the output on
155 // stderr may contain it.
156 if p, n, err := parsePos(msg); err == nil {
157 if abs, err := filepath.Abs(p.Filename); err == nil {
165 posn, _, err = parsePos(e.Pos)
167 panic(fmt.Sprintf("internal error: %s", e))
171 Diagnostic: runner.Diagnostic{
176 Severity: severityError,
178 problems = append(problems, p)
181 Diagnostic: runner.Diagnostic{
182 Position: token.Position{},
186 Severity: severityError,
188 problems = append(problems, p)
195 type unusedKey struct {
202 type unusedPair struct {
204 obj unused.SerializedObject
207 func success(allowedChecks map[string]bool, res runner.ResultData) []problem {
208 diags := res.Diagnostics
209 var problems []problem
210 for _, diag := range diags {
211 if !allowedChecks[diag.Category] {
214 problems = append(problems, problem{Diagnostic: diag})
219 func filterIgnored(problems []problem, res runner.ResultData, allowedAnalyzers map[string]bool) ([]problem, error) {
220 couldveMatched := func(ig *lineIgnore) bool {
221 for _, c := range ig.Checks {
223 // We never want to flag ignores for U1000,
224 // because U1000 isn't local to a single
225 // package. For example, an identifier may
226 // only be used by tests, in which case an
227 // ignore would only fire when not analyzing
228 // tests. To avoid spurious "useless ignore"
229 // warnings, just never flag U1000.
233 // Even though the runner always runs all analyzers, we
234 // still only flag unmatched ignores for the set of
235 // analyzers the user has expressed interest in. That way,
236 // `staticcheck -checks=SA1000` won't complain about an
237 // unmatched ignore for an unrelated check.
238 if allowedAnalyzers[c] {
246 ignores, moreProblems := parseDirectives(res.Directives)
248 for _, ig := range ignores {
249 for i := range problems {
252 p.Severity = severityIgnored
256 if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldveMatched(ig) {
258 Diagnostic: runner.Diagnostic{
260 Message: "this linter directive didn't match anything; should it be removed?",
261 Category: "staticcheck",
264 moreProblems = append(moreProblems, p)
268 return append(problems, moreProblems...), nil
271 func newLinter(cfg config.Config) (*linter, error) {
272 r, err := runner.New(cfg)
282 func (l *linter) SetGoVersion(n int) {
283 l.Runner.GoVersion = n
286 func (l *linter) Lint(cfg *packages.Config, patterns []string) (problems []problem, warnings []string, err error) {
287 results, err := l.Runner.Run(cfg, l.Checkers, patterns)
292 if len(results) == 0 && err == nil {
293 // TODO(dh): emulate Go's behavior more closely once we have
294 // access to go list's Match field.
295 for _, pattern := range patterns {
296 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
300 analyzerNames := make([]string, len(l.Checkers))
301 for i, a := range l.Checkers {
302 analyzerNames[i] = a.Name
305 used := map[unusedKey]bool{}
306 var unuseds []unusedPair
307 for _, res := range results {
308 if len(res.Errors) > 0 && !res.Failed {
309 panic("package has errors but isn't marked as failed")
312 problems = append(problems, failed(res)...)
315 warnings = append(warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
323 allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
324 resd, err := res.Load()
328 ps := success(allowedAnalyzers, resd)
329 filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
333 problems = append(problems, filtered...)
335 for _, obj := range resd.Unused.Used {
336 // FIXME(dh): pick the object whose filename does not include $GOROOT
338 pkgPath: res.Package.PkgPath,
339 base: filepath.Base(obj.Position.Filename),
340 line: obj.Position.Line,
346 if allowedAnalyzers["U1000"] {
347 for _, obj := range resd.Unused.Unused {
349 pkgPath: res.Package.PkgPath,
350 base: filepath.Base(obj.Position.Filename),
351 line: obj.Position.Line,
354 unuseds = append(unuseds, unusedPair{key, obj})
355 if _, ok := used[key]; !ok {
363 for _, uo := range unuseds {
367 if uo.obj.InGenerated {
370 problems = append(problems, problem{
371 Diagnostic: runner.Diagnostic{
372 Position: uo.obj.DisplayPosition,
373 Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
379 if len(problems) == 0 {
380 return nil, warnings, nil
383 sort.Slice(problems, func(i, j int) bool {
384 pi := problems[i].Position
385 pj := problems[j].Position
387 if pi.Filename != pj.Filename {
388 return pi.Filename < pj.Filename
390 if pi.Line != pj.Line {
391 return pi.Line < pj.Line
393 if pi.Column != pj.Column {
394 return pi.Column < pj.Column
397 return problems[i].Message < problems[j].Message
401 out = append(out, problems[0])
402 for i, p := range problems[1:] {
403 // We may encounter duplicate problems because one file
404 // can be part of many packages.
405 if !problems[i].equal(p) {
409 return out, warnings, nil
412 func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
413 allowedChecks := map[string]bool{}
415 for _, check := range checks {
417 if len(check) > 1 && check[0] == '-' {
421 if check == "*" || check == "all" {
423 for _, c := range analyzers {
426 } else if strings.HasSuffix(check, "*") {
428 prefix := check[:len(check)-1]
429 isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
431 for _, a := range analyzers {
432 idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
434 // Glob is S*, which should match S1000 but not SA1000
441 if strings.HasPrefix(a, prefix) {
447 // Literal check name
448 allowedChecks[check] = b
454 var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
456 func parsePos(pos string) (token.Position, int, error) {
457 if pos == "-" || pos == "" {
458 return token.Position{}, 0, nil
460 parts := posRe.FindStringSubmatch(pos)
462 return token.Position{}, 0, fmt.Errorf("internal error: malformed position %q", pos)
465 line, _ := strconv.Atoi(parts[2])
466 col, _ := strconv.Atoi(parts[3])
467 return token.Position{
471 }, len(parts[0]), nil
474 func usage(name string, flags *flag.FlagSet) func() {
476 fmt.Fprintf(os.Stderr, "Usage of %s:\n", name)
477 fmt.Fprintf(os.Stderr, "\t%s [flags] # runs on package in current directory\n", name)
478 fmt.Fprintf(os.Stderr, "\t%s [flags] packages\n", name)
479 fmt.Fprintf(os.Stderr, "\t%s [flags] directory\n", name)
480 fmt.Fprintf(os.Stderr, "\t%s [flags] files... # must be a single package\n", name)
481 fmt.Fprintf(os.Stderr, "Flags:\n")
482 flags.PrintDefaults()
488 func (list *list) String() string {
489 return `"` + strings.Join(*list, ",") + `"`
492 func (list *list) Set(s string) error {
498 *list = strings.Split(s, ",")
502 func FlagSet(name string) *flag.FlagSet {
503 flags := flag.NewFlagSet("", flag.ExitOnError)
504 flags.Usage = usage(name, flags)
505 flags.String("tags", "", "List of `build tags`")
506 flags.Bool("tests", true, "Include tests")
507 flags.Bool("version", false, "Print version and exit")
508 flags.Bool("show-ignored", false, "Don't filter ignored problems")
509 flags.String("f", "text", "Output `format` (valid choices are 'stylish', 'text' and 'json')")
510 flags.String("explain", "", "Print description of `check`")
512 flags.String("debug.cpuprofile", "", "Write CPU profile to `file`")
513 flags.String("debug.memprofile", "", "Write memory profile to `file`")
514 flags.Bool("debug.version", false, "Print detailed version information about this program")
515 flags.Bool("debug.no-compile-errors", false, "Don't print compile errors")
516 flags.String("debug.measure-analyzers", "", "Write analysis measurements to `file`. `file` will be opened for appending if it already exists.")
517 flags.String("debug.trace", "", "Write trace to `file`")
519 checks := list{"inherit"}
521 flags.Var(&checks, "checks", "Comma-separated list of `checks` to enable.")
522 flags.Var(&fail, "fail", "Comma-separated list of `checks` that can cause a non-zero exit status.")
524 tags := build.Default.ReleaseTags
525 v := tags[len(tags)-1][2:]
526 version := new(lint.VersionFlag)
527 if err := version.Set(v); err != nil {
528 panic(fmt.Sprintf("internal error: %s", err))
531 flags.Var(version, "go", "Target Go `version` in the format '1.x'")
535 func findCheck(cs []*analysis.Analyzer, check string) (*analysis.Analyzer, bool) {
536 for _, c := range cs {
544 func ProcessFlagSet(cs []*analysis.Analyzer, fs *flag.FlagSet) {
545 tags := fs.Lookup("tags").Value.(flag.Getter).Get().(string)
546 tests := fs.Lookup("tests").Value.(flag.Getter).Get().(bool)
547 goVersion := fs.Lookup("go").Value.(flag.Getter).Get().(int)
548 theFormatter := fs.Lookup("f").Value.(flag.Getter).Get().(string)
549 printVersion := fs.Lookup("version").Value.(flag.Getter).Get().(bool)
550 showIgnored := fs.Lookup("show-ignored").Value.(flag.Getter).Get().(bool)
551 explain := fs.Lookup("explain").Value.(flag.Getter).Get().(string)
553 cpuProfile := fs.Lookup("debug.cpuprofile").Value.(flag.Getter).Get().(string)
554 memProfile := fs.Lookup("debug.memprofile").Value.(flag.Getter).Get().(string)
555 debugVersion := fs.Lookup("debug.version").Value.(flag.Getter).Get().(bool)
556 debugNoCompile := fs.Lookup("debug.no-compile-errors").Value.(flag.Getter).Get().(bool)
557 traceOut := fs.Lookup("debug.trace").Value.(flag.Getter).Get().(string)
559 var measureAnalyzers func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
560 if path := fs.Lookup("debug.measure-analyzers").Value.(flag.Getter).Get().(string); path != "" {
561 f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
567 measureAnalyzers = func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration) {
570 // FIXME(dh): print pkg.ID
571 if _, err := fmt.Fprintf(f, "%s\t%s\t%d\n", analysis.Name, pkg, d.Nanoseconds()); err != nil {
572 log.Println("error writing analysis measurements:", err)
577 cfg := config.Config{}
578 cfg.Checks = *fs.Lookup("checks").Value.(*list)
580 exit := func(code int) {
581 if cpuProfile != "" {
582 pprof.StopCPUProfile()
584 if memProfile != "" {
585 f, err := os.Create(memProfile)
590 pprof.WriteHeapProfile(f)
597 if cpuProfile != "" {
598 f, err := os.Create(cpuProfile)
602 pprof.StartCPUProfile(f)
605 f, err := os.Create(traceOut)
622 // Validate that the tags argument is well-formed. go/packages
623 // doesn't detect malformed build flags and returns unhelpful
625 tf := buildutil.TagsFlag{}
626 if err := tf.Set(tags); err != nil {
627 fmt.Fprintln(os.Stderr, fmt.Errorf("invalid value %q for flag -tags: %s", tags, err))
632 var haystack []*analysis.Analyzer
633 haystack = append(haystack, cs...)
634 check, ok := findCheck(haystack, explain)
636 fmt.Fprintln(os.Stderr, "Couldn't find check", explain)
640 fmt.Fprintln(os.Stderr, explain, "has no documentation")
643 fmt.Println(check.Doc)
648 switch theFormatter {
650 f = textFormatter{W: os.Stdout}
652 f = &stylishFormatter{W: os.Stdout}
654 f = jsonFormatter{W: os.Stdout}
658 fmt.Fprintf(os.Stderr, "unsupported output format %q\n", theFormatter)
662 ps, warnings, err := doLint(cs, fs.Args(), &options{
665 GoVersion: goVersion,
667 PrintAnalyzerMeasurement: measureAnalyzers,
670 fmt.Fprintln(os.Stderr, err)
674 for _, w := range warnings {
675 fmt.Fprintln(os.Stderr, "warning:", w)
684 fail := *fs.Lookup("fail").Value.(*list)
685 analyzerNames := make([]string, len(cs))
686 for i, a := range cs {
687 analyzerNames[i] = a.Name
689 shouldExit := filterAnalyzerNames(analyzerNames, fail)
690 shouldExit["staticcheck"] = true
691 shouldExit["compile"] = true
693 for _, p := range ps {
694 if p.Category == "compile" && debugNoCompile {
697 if p.Severity == severityIgnored && !showIgnored {
701 if shouldExit[p.Category] {
704 p.Severity = severityWarning
709 if f, ok := f.(statter); ok {
710 f.Stats(len(ps), numErrors, numWarnings, numIgnored)
719 type options struct {
725 PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
728 func computeSalt() ([]byte, error) {
729 if version.Version != "devel" {
730 return []byte(version.Version), nil
732 p, err := os.Executable()
742 if _, err := io.Copy(h, f); err != nil {
745 return h.Sum(nil), nil
748 func doLint(cs []*analysis.Analyzer, paths []string, opt *options) ([]problem, []string, error) {
749 salt, err := computeSalt()
751 return nil, nil, fmt.Errorf("could not compute salt for cache: %s", err)
759 l, err := newLinter(opt.Config)
764 l.SetGoVersion(opt.GoVersion)
765 l.Runner.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
767 cfg := &packages.Config{}
772 cfg.BuildFlags = append(cfg.BuildFlags, "-tags", opt.Tags)
775 printStats := func() {
776 // Individual stats are read atomically, but overall there
777 // is no synchronisation. For printing rough progress
778 // information, this doesn't matter.
779 switch l.Runner.Stats.State() {
780 case runner.StateInitializing:
781 fmt.Fprintln(os.Stderr, "Status: initializing")
782 case runner.StateLoadPackageGraph:
783 fmt.Fprintln(os.Stderr, "Status: loading package graph")
784 case runner.StateBuildActionGraph:
785 fmt.Fprintln(os.Stderr, "Status: building action graph")
786 case runner.StateProcessing:
787 fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
788 l.Runner.Stats.ProcessedInitialPackages(),
789 l.Runner.Stats.InitialPackages(),
790 l.Runner.Stats.ProcessedPackages(),
791 l.Runner.Stats.TotalPackages(),
792 l.Runner.ActiveWorkers(),
793 l.Runner.TotalWorkers(),
795 case runner.StateFinalizing:
796 fmt.Fprintln(os.Stderr, "Status: finalizing")
799 if len(infoSignals) > 0 {
800 ch := make(chan os.Signal, 1)
801 signal.Notify(ch, infoSignals...)
802 defer signal.Stop(ch)
809 return l.Lint(cfg, paths)