lint.go (14476B)
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[caseFoldedString]*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 cache.SetSalt(salt) 70 71 analyzers := make(map[caseFoldedString]*lint.Analyzer, len(opts.analyzers)) 72 for _, a := range opts.analyzers { 73 analyzers[makeCaseFoldedString(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([]caseFoldedString, 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 resChecks := makeCaseFoldedStrings(res.Config.Checks) 201 allowedAnalyzers := filterAnalyzerNames(analyzerNames, resChecks) 202 resd, err := res.Load() 203 if err != nil { 204 return out, err 205 } 206 ps := success(allowedAnalyzers, resd) 207 filtered, err := filterIgnored(ps, resd, allowedAnalyzers) 208 if err != nil { 209 return out, err 210 } 211 // OPT move this code into the 'success' function. 212 for i, diag := range filtered { 213 a := l.analyzers[makeCaseFoldedString(diag.Category)] 214 // Some diag.Category don't map to analyzers, such as "staticcheck" 215 if a != nil { 216 filtered[i].MergeIf = a.Doc.MergeIf 217 } 218 } 219 out.Diagnostics = append(out.Diagnostics, filtered...) 220 221 for _, obj := range resd.Unused.Used { 222 // Note: a side-effect of this code is that fields in instantiated structs are handled correctly. Even 223 // if only an instantiated field is marked as used, we will not flag the generic field, because it has 224 // the same position as the instance. At some point this won't be necessary anymore because we'll be 225 // able to make use of the Go 1.19+ Origin methods. 226 227 // FIXME(dh): pick the object whose filename does not include $GOROOT 228 key := unusedKey{ 229 pkgPath: res.Package.PkgPath, 230 base: filepath.Base(obj.Position.Filename), 231 line: obj.Position.Line, 232 name: obj.Name, 233 } 234 used[key] = true 235 } 236 237 if allowedAnalyzers[makeCaseFoldedString("U1000")] { 238 for _, obj := range resd.Unused.Unused { 239 key := unusedKey{ 240 pkgPath: res.Package.PkgPath, 241 base: filepath.Base(obj.Position.Filename), 242 line: obj.Position.Line, 243 name: obj.Name, 244 } 245 unuseds = append(unuseds, unusedPair{key, obj}) 246 if _, ok := used[key]; !ok { 247 used[key] = false 248 } 249 } 250 } 251 } 252 } 253 254 for _, uo := range unuseds { 255 if used[uo.key] { 256 continue 257 } 258 out.Diagnostics = append(out.Diagnostics, diagnostic{ 259 Diagnostic: runner.Diagnostic{ 260 Position: uo.obj.DisplayPosition, 261 Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name), 262 Category: "U1000", 263 }, 264 MergeIf: lint.MergeIfAll, 265 }) 266 } 267 268 return out, nil 269 } 270 271 func filterIgnored( 272 diagnostics []diagnostic, 273 res runner.ResultData, 274 allowedAnalyzers map[caseFoldedString]bool, 275 ) ([]diagnostic, error) { 276 couldHaveMatched := func(ig *lineIgnore) bool { 277 for _, c := range ig.Checks { 278 if c.String() == "u1000" { 279 // We never want to flag ignores for U1000, 280 // because U1000 isn't local to a single 281 // package. For example, an identifier may 282 // only be used by tests, in which case an 283 // ignore would only fire when not analyzing 284 // tests. To avoid spurious "useless ignore" 285 // warnings, just never flag U1000. 286 return false 287 } 288 289 // Even though the runner always runs all analyzers, we 290 // still only flag unmatched ignores for the set of 291 // analyzers the user has expressed interest in. That way, 292 // `staticcheck -checks=SA1000` won't complain about an 293 // unmatched ignore for an unrelated check. 294 if allowedAnalyzers[c] { 295 return true 296 } 297 } 298 299 return false 300 } 301 302 ignores, moreDiagnostics := parseDirectives(res.Directives) 303 304 for _, ig := range ignores { 305 for i := range diagnostics { 306 diag := &diagnostics[i] 307 if ig.match(*diag) { 308 diag.Severity = severityIgnored 309 } 310 } 311 312 if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) { 313 diag := diagnostic{ 314 Diagnostic: runner.Diagnostic{ 315 Position: ig.Pos, 316 Message: "this linter directive didn't match anything; should it be removed?", 317 Category: "staticcheck", 318 }, 319 } 320 moreDiagnostics = append(moreDiagnostics, diag) 321 } 322 } 323 324 return append(diagnostics, moreDiagnostics...), nil 325 } 326 327 type ignore interface { 328 match(diag diagnostic) bool 329 } 330 331 type lineIgnore struct { 332 File string 333 Line int 334 Checks []caseFoldedString 335 Matched bool 336 Pos token.Position 337 } 338 339 func (li *lineIgnore) match(p diagnostic) bool { 340 pos := p.Position 341 if pos.Filename != li.File || pos.Line != li.Line { 342 return false 343 } 344 for _, c := range li.Checks { 345 if m, _ := filepath.Match(c.String(), makeCaseFoldedString(p.Category).String()); m { 346 li.Matched = true 347 return true 348 } 349 } 350 return false 351 } 352 353 type fileIgnore struct { 354 File string 355 Checks []caseFoldedString 356 } 357 358 func (fi *fileIgnore) match(p diagnostic) bool { 359 if p.Position.Filename != fi.File { 360 return false 361 } 362 for _, c := range fi.Checks { 363 if m, _ := filepath.Match(c.String(), makeCaseFoldedString(p.Category).String()); m { 364 return true 365 } 366 } 367 return false 368 } 369 370 type severity uint8 371 372 const ( 373 severityError severity = iota 374 severityWarning 375 severityIgnored 376 ) 377 378 func (s severity) String() string { 379 switch s { 380 case severityError: 381 return "error" 382 case severityWarning: 383 return "warning" 384 case severityIgnored: 385 return "ignored" 386 default: 387 return fmt.Sprintf("Severity(%d)", s) 388 } 389 } 390 391 // diagnostic represents a diagnostic in some source code. 392 type diagnostic struct { 393 runner.Diagnostic 394 395 // These fields are exported so that we can gob encode them. 396 Severity severity 397 MergeIf lint.MergeStrategy 398 BuildName string 399 } 400 401 func (p diagnostic) equal(o diagnostic) bool { 402 return p.Position == o.Position && 403 p.End == o.End && 404 p.Message == o.Message && 405 makeCaseFoldedString(p.Category) == makeCaseFoldedString(o.Category) && 406 p.Severity == o.Severity && 407 p.MergeIf == o.MergeIf && 408 p.BuildName == o.BuildName 409 } 410 411 func (p *diagnostic) String() string { 412 if p.BuildName != "" { 413 return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category) 414 } else { 415 return fmt.Sprintf("%s (%s)", p.Message, p.Category) 416 } 417 } 418 419 func failed(res runner.Result) []diagnostic { 420 var diagnostics []diagnostic 421 422 for _, e := range res.Errors { 423 switch e := e.(type) { 424 case packages.Error: 425 msg := e.Msg 426 if len(msg) != 0 && msg[0] == '\n' { 427 // TODO(dh): See https://github.com/golang/go/issues/32363 428 msg = msg[1:] 429 } 430 431 cat := "compile" 432 if e.Kind == packages.ParseError { 433 cat = "config" 434 } 435 436 var posn token.Position 437 if e.Pos == "" { 438 // Under certain conditions (malformed package 439 // declarations, multiple packages in the same 440 // directory), go list emits an error on stderr 441 // instead of JSON. Those errors do not have 442 // associated position information in 443 // go/packages.Error, even though the output on 444 // stderr may contain it. 445 if p, n, err := parsePos(msg); err == nil { 446 if abs, err := filepath.Abs(p.Filename); err == nil { 447 p.Filename = abs 448 } 449 posn = p 450 msg = msg[n+2:] 451 } 452 } else { 453 var err error 454 posn, _, err = parsePos(e.Pos) 455 if err != nil { 456 panic(fmt.Sprintf("internal error: %s", err)) 457 } 458 } 459 diag := diagnostic{ 460 Diagnostic: runner.Diagnostic{ 461 Position: posn, 462 Message: msg, 463 Category: cat, 464 }, 465 Severity: severityError, 466 } 467 diagnostics = append(diagnostics, diag) 468 case error: 469 diag := diagnostic{ 470 Diagnostic: runner.Diagnostic{ 471 Position: token.Position{}, 472 Message: e.Error(), 473 Category: "compile", 474 }, 475 Severity: severityError, 476 } 477 diagnostics = append(diagnostics, diag) 478 } 479 } 480 481 return diagnostics 482 } 483 484 type unusedKey struct { 485 pkgPath string 486 base string 487 line int 488 name string 489 } 490 491 type unusedPair struct { 492 key unusedKey 493 obj unused.Object 494 } 495 496 func success(allowedAnalyzers map[caseFoldedString]bool, res runner.ResultData) []diagnostic { 497 diags := res.Diagnostics 498 var diagnostics []diagnostic 499 for _, diag := range diags { 500 if !allowedAnalyzers[makeCaseFoldedString(diag.Category)] { 501 continue 502 } 503 diagnostics = append(diagnostics, diagnostic{Diagnostic: diag}) 504 } 505 return diagnostics 506 } 507 508 func filterAnalyzerNames(allAnalyzers []caseFoldedString, selection []caseFoldedString) map[caseFoldedString]bool { 509 allowedChecks := map[caseFoldedString]bool{} 510 511 for _, check := range selection { 512 b := true 513 if check.Length() > 1 && check.Index(0) == '-' { 514 b = false 515 check = check.Slice(1, -1) 516 } 517 if check.String() == "*" || check.String() == "all" { 518 // Match all 519 for _, a := range allAnalyzers { 520 allowedChecks[a] = b 521 } 522 } else if strings.HasSuffix(check.String(), "*") { 523 // Glob 524 prefix := check.Slice(0, check.Length()-1) 525 isCat := strings.IndexFunc(prefix.String(), unicode.IsNumber) == -1 526 527 for _, a := range allAnalyzers { 528 idx := strings.IndexFunc(a.String(), unicode.IsNumber) 529 if isCat { 530 // Glob is S*, which should match S1000 but not SA1000 531 cat := a.Slice(0, idx) 532 if prefix == cat { 533 allowedChecks[a] = b 534 } 535 } else { 536 // Glob is S1* 537 if strings.HasPrefix(a.String(), prefix.String()) { 538 allowedChecks[a] = b 539 } 540 } 541 } 542 } else { 543 // Literal check name 544 allowedChecks[check] = b 545 } 546 } 547 return allowedChecks 548 } 549 550 // Note that the file name is optional and can be empty because of //line 551 // directives of the form "//line :1" (but not "//line :1:1"). See 552 // https://go.dev/issue/24183 and https://staticcheck.dev/issues/1582. 553 var posRe = regexp.MustCompile(`^(?:(.+?):)?(\d+)(?::(\d+)?)?`) 554 555 func parsePos(pos string) (token.Position, int, error) { 556 if pos == "-" || pos == "" { 557 return token.Position{}, 0, nil 558 } 559 parts := posRe.FindStringSubmatch(pos) 560 if parts == nil { 561 return token.Position{}, 0, fmt.Errorf("malformed position %q", pos) 562 } 563 file := parts[1] 564 line, _ := strconv.Atoi(parts[2]) 565 col, _ := strconv.Atoi(parts[3]) 566 return token.Position{ 567 Filename: file, 568 Line: line, 569 Column: col, 570 }, len(parts[0]), nil 571 }