lint.go raw
1 package lintcmd
2
3 import (
4 "crypto/sha256"
5 "fmt"
6 "go/token"
7 "io"
8 "os"
9 "os/signal"
10 "path/filepath"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15 "unicode"
16
17 "honnef.co/go/tools/analysis/lint"
18 "honnef.co/go/tools/config"
19 "honnef.co/go/tools/go/buildid"
20 "honnef.co/go/tools/go/loader"
21 "honnef.co/go/tools/lintcmd/cache"
22 "honnef.co/go/tools/lintcmd/runner"
23 "honnef.co/go/tools/unused"
24
25 "golang.org/x/tools/go/analysis"
26 "golang.org/x/tools/go/packages"
27 )
28
29 // A linter lints Go source code.
30 type linter struct {
31 analyzers map[string]*lint.Analyzer
32 cache *cache.Cache
33 opts options
34 }
35
36 func computeSalt() ([]byte, error) {
37 p, err := os.Executable()
38 if err != nil {
39 return nil, err
40 }
41
42 if id, err := buildid.ReadFile(p); err == nil {
43 return []byte(id), nil
44 } else {
45 // For some reason we couldn't read the build id from the executable.
46 // Fall back to hashing the entire executable.
47 f, err := os.Open(p)
48 if err != nil {
49 return nil, err
50 }
51 defer f.Close()
52 h := sha256.New()
53 if _, err := io.Copy(h, f); err != nil {
54 return nil, err
55 }
56 return h.Sum(nil), nil
57 }
58 }
59
60 func newLinter(opts options) (*linter, error) {
61 c, err := cache.Default()
62 if err != nil {
63 return nil, err
64 }
65 salt, err := computeSalt()
66 if err != nil {
67 return nil, fmt.Errorf("could not compute salt for cache: %s", err)
68 }
69 c.SetSalt(salt)
70
71 analyzers := make(map[string]*lint.Analyzer, len(opts.analyzers))
72 for _, a := range opts.analyzers {
73 analyzers[a.Analyzer.Name] = a
74 }
75
76 return &linter{
77 cache: c,
78 analyzers: analyzers,
79 opts: opts,
80 }, nil
81 }
82
83 type lintResult struct {
84 // These fields are exported so that we can gob encode them.
85
86 CheckedFiles []string
87 Diagnostics []diagnostic
88 Warnings []string
89 }
90
91 type options struct {
92 config config.Config
93 analyzers []*lint.Analyzer
94 patterns []string
95 lintTests bool
96 goVersion string
97 printAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
98 }
99
100 func (l *linter) run(bconf buildConfig) (lintResult, error) {
101 cfg := &packages.Config{}
102 if l.opts.lintTests {
103 cfg.Tests = true
104 }
105
106 cfg.BuildFlags = bconf.Flags
107 cfg.Env = append(os.Environ(), bconf.Envs...)
108
109 r, err := runner.New(l.opts.config, l.cache)
110 if err != nil {
111 return lintResult{}, err
112 }
113 r.GoVersion = l.opts.goVersion
114 r.Stats.PrintAnalyzerMeasurement = l.opts.printAnalyzerMeasurement
115
116 printStats := func() {
117 // Individual stats are read atomically, but overall there
118 // is no synchronisation. For printing rough progress
119 // information, this doesn't matter.
120 switch r.Stats.State() {
121 case runner.StateInitializing:
122 fmt.Fprintln(os.Stderr, "Status: initializing")
123 case runner.StateLoadPackageGraph:
124 fmt.Fprintln(os.Stderr, "Status: loading package graph")
125 case runner.StateBuildActionGraph:
126 fmt.Fprintln(os.Stderr, "Status: building action graph")
127 case runner.StateProcessing:
128 fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
129 r.Stats.ProcessedInitialPackages(),
130 r.Stats.InitialPackages(),
131 r.Stats.ProcessedPackages(),
132 r.Stats.TotalPackages(),
133 r.ActiveWorkers(),
134 r.TotalWorkers(),
135 )
136 case runner.StateFinalizing:
137 fmt.Fprintln(os.Stderr, "Status: finalizing")
138 }
139 }
140 if len(infoSignals) > 0 {
141 ch := make(chan os.Signal, 1)
142 signal.Notify(ch, infoSignals...)
143 defer signal.Stop(ch)
144 go func() {
145 for range ch {
146 printStats()
147 }
148 }()
149 }
150 res, err := l.lint(r, cfg, l.opts.patterns)
151 for i := range res.Diagnostics {
152 res.Diagnostics[i].BuildName = bconf.Name
153 }
154 return res, err
155 }
156
157 func (l *linter) lint(r *runner.Runner, cfg *packages.Config, patterns []string) (lintResult, error) {
158 var out lintResult
159
160 as := make([]*analysis.Analyzer, 0, len(l.analyzers))
161 for _, a := range l.analyzers {
162 as = append(as, a.Analyzer)
163 }
164 results, err := r.Run(cfg, as, patterns)
165 if err != nil {
166 return out, err
167 }
168
169 if len(results) == 0 {
170 // TODO(dh): emulate Go's behavior more closely once we have
171 // access to go list's Match field.
172 for _, pattern := range patterns {
173 fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
174 }
175 }
176
177 analyzerNames := make([]string, 0, len(l.analyzers))
178 for name := range l.analyzers {
179 analyzerNames = append(analyzerNames, name)
180 }
181 used := map[unusedKey]bool{}
182 var unuseds []unusedPair
183 for _, res := range results {
184 if len(res.Errors) > 0 && !res.Failed {
185 panic("package has errors but isn't marked as failed")
186 }
187 if res.Failed {
188 out.Diagnostics = append(out.Diagnostics, failed(res)...)
189 } else {
190 if res.Skipped {
191 out.Warnings = append(out.Warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
192 continue
193 }
194
195 if !res.Initial {
196 continue
197 }
198
199 out.CheckedFiles = append(out.CheckedFiles, res.Package.GoFiles...)
200 allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
201 resd, err := res.Load()
202 if err != nil {
203 return out, err
204 }
205 ps := success(allowedAnalyzers, resd)
206 filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
207 if err != nil {
208 return out, err
209 }
210 // OPT move this code into the 'success' function.
211 for i, diag := range filtered {
212 a := l.analyzers[diag.Category]
213 // Some diag.Category don't map to analyzers, such as "staticcheck"
214 if a != nil {
215 filtered[i].MergeIf = a.Doc.MergeIf
216 }
217 }
218 out.Diagnostics = append(out.Diagnostics, filtered...)
219
220 for _, obj := range resd.Unused.Used {
221 // Note: a side-effect of this code is that fields in instantiated structs are handled correctly. Even
222 // if only an instantiated field is marked as used, we will not flag the generic field, because it has
223 // the same position as the instance. At some point this won't be necessary anymore because we'll be
224 // able to make use of the Go 1.19+ Origin methods.
225
226 // FIXME(dh): pick the object whose filename does not include $GOROOT
227 key := unusedKey{
228 pkgPath: res.Package.PkgPath,
229 base: filepath.Base(obj.Position.Filename),
230 line: obj.Position.Line,
231 name: obj.Name,
232 }
233 used[key] = true
234 }
235
236 if allowedAnalyzers["U1000"] {
237 for _, obj := range resd.Unused.Unused {
238 key := unusedKey{
239 pkgPath: res.Package.PkgPath,
240 base: filepath.Base(obj.Position.Filename),
241 line: obj.Position.Line,
242 name: obj.Name,
243 }
244 unuseds = append(unuseds, unusedPair{key, obj})
245 if _, ok := used[key]; !ok {
246 used[key] = false
247 }
248 }
249 }
250 }
251 }
252
253 for _, uo := range unuseds {
254 if used[uo.key] {
255 continue
256 }
257 out.Diagnostics = append(out.Diagnostics, diagnostic{
258 Diagnostic: runner.Diagnostic{
259 Position: uo.obj.DisplayPosition,
260 Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
261 Category: "U1000",
262 },
263 MergeIf: lint.MergeIfAll,
264 })
265 }
266
267 return out, nil
268 }
269
270 func filterIgnored(diagnostics []diagnostic, res runner.ResultData, allowedAnalyzers map[string]bool) ([]diagnostic, error) {
271 couldHaveMatched := func(ig *lineIgnore) bool {
272 for _, c := range ig.Checks {
273 if c == "U1000" {
274 // We never want to flag ignores for U1000,
275 // because U1000 isn't local to a single
276 // package. For example, an identifier may
277 // only be used by tests, in which case an
278 // ignore would only fire when not analyzing
279 // tests. To avoid spurious "useless ignore"
280 // warnings, just never flag U1000.
281 return false
282 }
283
284 // Even though the runner always runs all analyzers, we
285 // still only flag unmatched ignores for the set of
286 // analyzers the user has expressed interest in. That way,
287 // `staticcheck -checks=SA1000` won't complain about an
288 // unmatched ignore for an unrelated check.
289 if allowedAnalyzers[c] {
290 return true
291 }
292 }
293
294 return false
295 }
296
297 ignores, moreDiagnostics := parseDirectives(res.Directives)
298
299 for _, ig := range ignores {
300 for i := range diagnostics {
301 diag := &diagnostics[i]
302 if ig.match(*diag) {
303 diag.Severity = severityIgnored
304 }
305 }
306
307 if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) {
308 diag := diagnostic{
309 Diagnostic: runner.Diagnostic{
310 Position: ig.Pos,
311 Message: "this linter directive didn't match anything; should it be removed?",
312 Category: "staticcheck",
313 },
314 }
315 moreDiagnostics = append(moreDiagnostics, diag)
316 }
317 }
318
319 return append(diagnostics, moreDiagnostics...), nil
320 }
321
322 type ignore interface {
323 match(diag diagnostic) bool
324 }
325
326 type lineIgnore struct {
327 File string
328 Line int
329 Checks []string
330 Matched bool
331 Pos token.Position
332 }
333
334 func (li *lineIgnore) match(p diagnostic) bool {
335 pos := p.Position
336 if pos.Filename != li.File || pos.Line != li.Line {
337 return false
338 }
339 for _, c := range li.Checks {
340 if m, _ := filepath.Match(c, p.Category); m {
341 li.Matched = true
342 return true
343 }
344 }
345 return false
346 }
347
348 func (li *lineIgnore) String() string {
349 matched := "not matched"
350 if li.Matched {
351 matched = "matched"
352 }
353 return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
354 }
355
356 type fileIgnore struct {
357 File string
358 Checks []string
359 }
360
361 func (fi *fileIgnore) match(p diagnostic) bool {
362 if p.Position.Filename != fi.File {
363 return false
364 }
365 for _, c := range fi.Checks {
366 if m, _ := filepath.Match(c, p.Category); m {
367 return true
368 }
369 }
370 return false
371 }
372
373 type severity uint8
374
375 const (
376 severityError severity = iota
377 severityWarning
378 severityIgnored
379 )
380
381 func (s severity) String() string {
382 switch s {
383 case severityError:
384 return "error"
385 case severityWarning:
386 return "warning"
387 case severityIgnored:
388 return "ignored"
389 default:
390 return fmt.Sprintf("Severity(%d)", s)
391 }
392 }
393
394 // diagnostic represents a diagnostic in some source code.
395 type diagnostic struct {
396 runner.Diagnostic
397
398 // These fields are exported so that we can gob encode them.
399 Severity severity
400 MergeIf lint.MergeStrategy
401 BuildName string
402 }
403
404 func (p diagnostic) equal(o diagnostic) bool {
405 return p.Position == o.Position &&
406 p.End == o.End &&
407 p.Message == o.Message &&
408 p.Category == o.Category &&
409 p.Severity == o.Severity &&
410 p.MergeIf == o.MergeIf &&
411 p.BuildName == o.BuildName
412 }
413
414 func (p *diagnostic) String() string {
415 if p.BuildName != "" {
416 return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category)
417 } else {
418 return fmt.Sprintf("%s (%s)", p.Message, p.Category)
419 }
420 }
421
422 func failed(res runner.Result) []diagnostic {
423 var diagnostics []diagnostic
424
425 for _, e := range res.Errors {
426 switch e := e.(type) {
427 case packages.Error:
428 msg := e.Msg
429 if len(msg) != 0 && msg[0] == '\n' {
430 // TODO(dh): See https://github.com/golang/go/issues/32363
431 msg = msg[1:]
432 }
433
434 cat := "compile"
435 if e.Kind == packages.ParseError {
436 cat = "config"
437 }
438
439 var posn token.Position
440 if e.Pos == "" {
441 // Under certain conditions (malformed package
442 // declarations, multiple packages in the same
443 // directory), go list emits an error on stderr
444 // instead of JSON. Those errors do not have
445 // associated position information in
446 // go/packages.Error, even though the output on
447 // stderr may contain it.
448 if p, n, err := parsePos(msg); err == nil {
449 if abs, err := filepath.Abs(p.Filename); err == nil {
450 p.Filename = abs
451 }
452 posn = p
453 msg = msg[n+2:]
454 }
455 } else {
456 var err error
457 posn, _, err = parsePos(e.Pos)
458 if err != nil {
459 panic(fmt.Sprintf("internal error: %s", err))
460 }
461 }
462 diag := diagnostic{
463 Diagnostic: runner.Diagnostic{
464 Position: posn,
465 Message: msg,
466 Category: cat,
467 },
468 Severity: severityError,
469 }
470 diagnostics = append(diagnostics, diag)
471 case error:
472 diag := diagnostic{
473 Diagnostic: runner.Diagnostic{
474 Position: token.Position{},
475 Message: e.Error(),
476 Category: "compile",
477 },
478 Severity: severityError,
479 }
480 diagnostics = append(diagnostics, diag)
481 }
482 }
483
484 return diagnostics
485 }
486
487 type unusedKey struct {
488 pkgPath string
489 base string
490 line int
491 name string
492 }
493
494 type unusedPair struct {
495 key unusedKey
496 obj unused.Object
497 }
498
499 func success(allowedAnalyzers map[string]bool, res runner.ResultData) []diagnostic {
500 diags := res.Diagnostics
501 var diagnostics []diagnostic
502 for _, diag := range diags {
503 if !allowedAnalyzers[diag.Category] {
504 continue
505 }
506 diagnostics = append(diagnostics, diagnostic{Diagnostic: diag})
507 }
508 return diagnostics
509 }
510
511 func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
512 allowedChecks := map[string]bool{}
513
514 for _, check := range checks {
515 b := true
516 if len(check) > 1 && check[0] == '-' {
517 b = false
518 check = check[1:]
519 }
520 if check == "*" || check == "all" {
521 // Match all
522 for _, c := range analyzers {
523 allowedChecks[c] = b
524 }
525 } else if strings.HasSuffix(check, "*") {
526 // Glob
527 prefix := check[:len(check)-1]
528 isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
529
530 for _, a := range analyzers {
531 idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
532 if isCat {
533 // Glob is S*, which should match S1000 but not SA1000
534 cat := a[:idx]
535 if prefix == cat {
536 allowedChecks[a] = b
537 }
538 } else {
539 // Glob is S1*
540 if strings.HasPrefix(a, prefix) {
541 allowedChecks[a] = b
542 }
543 }
544 }
545 } else {
546 // Literal check name
547 allowedChecks[check] = b
548 }
549 }
550 return allowedChecks
551 }
552
553 // Note that the file name is optional and can be empty because of //line
554 // directives of the form "//line :1" (but not "//line :1:1"). See
555 // https://go.dev/issue/24183 and https://staticcheck.dev/issues/1582.
556 var posRe = regexp.MustCompile(`^(?:(.+?):)?(\d+)(?::(\d+)?)?`)
557
558 func parsePos(pos string) (token.Position, int, error) {
559 if pos == "-" || pos == "" {
560 return token.Position{}, 0, nil
561 }
562 parts := posRe.FindStringSubmatch(pos)
563 if parts == nil {
564 return token.Position{}, 0, fmt.Errorf("malformed position %q", pos)
565 }
566 file := parts[1]
567 line, _ := strconv.Atoi(parts[2])
568 col, _ := strconv.Atoi(parts[3])
569 return token.Position{
570 Filename: file,
571 Line: line,
572 Column: col,
573 }, len(parts[0]), nil
574 }
575