github.com/golangci/go-tools@v0.0.0-20190318060251-af6baa5dc196/lint/lint.go (about) 1 // Package lint provides the foundation for tools like staticcheck 2 package lint // import "github.com/golangci/go-tools/lint" 3 4 import ( 5 "fmt" 6 "go/ast" 7 "go/token" 8 "go/types" 9 "io" 10 "os" 11 "path/filepath" 12 "runtime/debug" 13 "sort" 14 "strings" 15 "sync" 16 "time" 17 "unicode" 18 19 "github.com/golangci/go-tools/config" 20 "github.com/golangci/go-tools/ssa" 21 "github.com/golangci/go-tools/ssa/ssautil" 22 "golang.org/x/tools/go/packages" 23 ) 24 25 type Job struct { 26 Program *Program 27 28 checker string 29 check Check 30 problems []Problem 31 32 duration time.Duration 33 panicErr error 34 } 35 36 type Ignore interface { 37 Match(p Problem) bool 38 } 39 40 type LineIgnore struct { 41 File string 42 Line int 43 Checks []string 44 matched bool 45 pos token.Pos 46 } 47 48 func (li *LineIgnore) Match(p Problem) bool { 49 if p.Position.Filename != li.File || p.Position.Line != li.Line { 50 return false 51 } 52 for _, c := range li.Checks { 53 if m, _ := filepath.Match(c, p.Check); m { 54 li.matched = true 55 return true 56 } 57 } 58 return false 59 } 60 61 func (li *LineIgnore) String() string { 62 matched := "not matched" 63 if li.matched { 64 matched = "matched" 65 } 66 return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched) 67 } 68 69 type FileIgnore struct { 70 File string 71 Checks []string 72 } 73 74 func (fi *FileIgnore) Match(p Problem) bool { 75 if p.Position.Filename != fi.File { 76 return false 77 } 78 for _, c := range fi.Checks { 79 if m, _ := filepath.Match(c, p.Check); m { 80 return true 81 } 82 } 83 return false 84 } 85 86 type GlobIgnore struct { 87 Pattern string 88 Checks []string 89 } 90 91 func (gi *GlobIgnore) Match(p Problem) bool { 92 if gi.Pattern != "*" { 93 pkgpath := p.Package.Types.Path() 94 if strings.HasSuffix(pkgpath, "_test") { 95 pkgpath = pkgpath[:len(pkgpath)-len("_test")] 96 } 97 name := filepath.Join(pkgpath, filepath.Base(p.Position.Filename)) 98 if m, _ := filepath.Match(gi.Pattern, name); !m { 99 return false 100 } 101 } 102 for _, c := range gi.Checks { 103 if m, _ := filepath.Match(c, p.Check); m { 104 return true 105 } 106 } 107 return false 108 } 109 110 type Program struct { 111 SSA *ssa.Program 112 InitialPackages []*Pkg 113 InitialFunctions []*ssa.Function 114 AllPackages []*packages.Package 115 AllFunctions []*ssa.Function 116 Files []*ast.File 117 GoVersion int 118 119 tokenFileMap map[*token.File]*ast.File 120 astFileMap map[*ast.File]*Pkg 121 packagesMap map[string]*packages.Package 122 123 genMu sync.RWMutex 124 generatedMap map[string]bool 125 } 126 127 func (prog *Program) Fset() *token.FileSet { 128 return prog.InitialPackages[0].Fset 129 } 130 131 type Func func(*Job) 132 133 type Severity uint8 134 135 const ( 136 Error Severity = iota 137 Warning 138 Ignored 139 ) 140 141 // Problem represents a problem in some source code. 142 type Problem struct { 143 Position token.Position // position in source file 144 Text string // the prose that describes the problem 145 Check string 146 Checker string 147 Package *Pkg 148 Severity Severity 149 } 150 151 func (p *Problem) String() string { 152 if p.Check == "" { 153 return p.Text 154 } 155 return fmt.Sprintf("%s (%s)", p.Text, p.Check) 156 } 157 158 type Checker interface { 159 Name() string 160 Prefix() string 161 Init(*Program) 162 Checks() []Check 163 } 164 165 type Check struct { 166 Fn Func 167 ID string 168 FilterGenerated bool 169 } 170 171 // A Linter lints Go source code. 172 type Linter struct { 173 Checkers []Checker 174 Ignores []Ignore 175 GoVersion int 176 ReturnIgnored bool 177 Config config.Config 178 179 MaxConcurrentJobs int 180 PrintStats bool 181 182 automaticIgnores []Ignore 183 } 184 185 func (l *Linter) ignore(p Problem) bool { 186 ignored := false 187 for _, ig := range l.automaticIgnores { 188 // We cannot short-circuit these, as we want to record, for 189 // each ignore, whether it matched or not. 190 if ig.Match(p) { 191 ignored = true 192 } 193 } 194 if ignored { 195 // no need to execute other ignores if we've already had a 196 // match. 197 return true 198 } 199 for _, ig := range l.Ignores { 200 // We can short-circuit here, as we aren't tracking any 201 // information. 202 if ig.Match(p) { 203 return true 204 } 205 } 206 207 return false 208 } 209 210 func (prog *Program) File(node Positioner) *ast.File { 211 return prog.tokenFileMap[prog.SSA.Fset.File(node.Pos())] 212 } 213 214 func (j *Job) File(node Positioner) *ast.File { 215 return j.Program.File(node) 216 } 217 218 func parseDirective(s string) (cmd string, args []string) { 219 if !strings.HasPrefix(s, "//lint:") { 220 return "", nil 221 } 222 s = strings.TrimPrefix(s, "//lint:") 223 fields := strings.Split(s, " ") 224 return fields[0], fields[1:] 225 } 226 227 type PerfStats struct { 228 PackageLoading time.Duration 229 SSABuild time.Duration 230 OtherInitWork time.Duration 231 CheckerInits map[string]time.Duration 232 Jobs []JobStat 233 } 234 235 type JobStat struct { 236 Job string 237 Duration time.Duration 238 } 239 240 func (stats *PerfStats) Print(w io.Writer) { 241 fmt.Fprintln(w, "Package loading:", stats.PackageLoading) 242 fmt.Fprintln(w, "SSA build:", stats.SSABuild) 243 fmt.Fprintln(w, "Other init work:", stats.OtherInitWork) 244 245 fmt.Fprintln(w, "Checker inits:") 246 for checker, d := range stats.CheckerInits { 247 fmt.Fprintf(w, "\t%s: %s\n", checker, d) 248 } 249 fmt.Fprintln(w) 250 251 fmt.Fprintln(w, "Jobs:") 252 sort.Slice(stats.Jobs, func(i, j int) bool { 253 return stats.Jobs[i].Duration < stats.Jobs[j].Duration 254 }) 255 var total time.Duration 256 for _, job := range stats.Jobs { 257 fmt.Fprintf(w, "\t%s: %s\n", job.Job, job.Duration) 258 total += job.Duration 259 } 260 fmt.Fprintf(w, "\tTotal: %s\n", total) 261 } 262 263 func (l *Linter) Lint(initial []*packages.Package, stats *PerfStats) []Problem { 264 allPkgs := allPackages(initial) 265 t := time.Now() 266 ssaprog, _ := ssautil.Packages(allPkgs, ssa.GlobalDebug) 267 ssaprog.Build() 268 if stats != nil { 269 stats.SSABuild = time.Since(t) 270 } 271 272 t = time.Now() 273 pkgMap := map[*ssa.Package]*Pkg{} 274 var pkgs []*Pkg 275 for _, pkg := range initial { 276 ssapkg := ssaprog.Package(pkg.Types) 277 var cfg config.Config 278 if len(pkg.GoFiles) != 0 { 279 path := pkg.GoFiles[0] 280 dir := filepath.Dir(path) 281 var err error 282 // OPT(dh): we're rebuilding the entire config tree for 283 // each package. for example, if we check a/b/c and 284 // a/b/c/d, we'll process a, a/b, a/b/c, a, a/b, a/b/c, 285 // a/b/c/d – we should cache configs per package and only 286 // load the new levels. 287 cfg, err = config.Load(dir) 288 if err != nil { 289 // FIXME(dh): we couldn't load the config, what are we 290 // supposed to do? probably tell the user somehow 291 } 292 cfg = cfg.Merge(l.Config) 293 } 294 295 pkg := &Pkg{ 296 SSA: ssapkg, 297 Package: pkg, 298 Config: cfg, 299 } 300 pkgMap[ssapkg] = pkg 301 pkgs = append(pkgs, pkg) 302 } 303 304 prog := &Program{ 305 SSA: ssaprog, 306 InitialPackages: pkgs, 307 AllPackages: allPkgs, 308 GoVersion: l.GoVersion, 309 tokenFileMap: map[*token.File]*ast.File{}, 310 astFileMap: map[*ast.File]*Pkg{}, 311 generatedMap: map[string]bool{}, 312 } 313 prog.packagesMap = map[string]*packages.Package{} 314 for _, pkg := range allPkgs { 315 prog.packagesMap[pkg.Types.Path()] = pkg 316 } 317 318 isInitial := map[*types.Package]struct{}{} 319 for _, pkg := range pkgs { 320 isInitial[pkg.Types] = struct{}{} 321 } 322 for fn := range ssautil.AllFunctions(ssaprog) { 323 if fn.Pkg == nil { 324 continue 325 } 326 prog.AllFunctions = append(prog.AllFunctions, fn) 327 if _, ok := isInitial[fn.Pkg.Pkg]; ok { 328 prog.InitialFunctions = append(prog.InitialFunctions, fn) 329 } 330 } 331 for _, pkg := range pkgs { 332 prog.Files = append(prog.Files, pkg.Syntax...) 333 334 ssapkg := ssaprog.Package(pkg.Types) 335 for _, f := range pkg.Syntax { 336 prog.astFileMap[f] = pkgMap[ssapkg] 337 } 338 } 339 340 for _, pkg := range allPkgs { 341 for _, f := range pkg.Syntax { 342 tf := pkg.Fset.File(f.Pos()) 343 prog.tokenFileMap[tf] = f 344 } 345 } 346 347 var out []Problem 348 l.automaticIgnores = nil 349 for _, pkg := range initial { 350 for _, f := range pkg.Syntax { 351 cm := ast.NewCommentMap(pkg.Fset, f, f.Comments) 352 for node, cgs := range cm { 353 for _, cg := range cgs { 354 for _, c := range cg.List { 355 if !strings.HasPrefix(c.Text, "//lint:") { 356 continue 357 } 358 cmd, args := parseDirective(c.Text) 359 switch cmd { 360 case "ignore", "file-ignore": 361 if len(args) < 2 { 362 // FIXME(dh): this causes duplicated warnings when using megacheck 363 p := Problem{ 364 Position: prog.DisplayPosition(c.Pos()), 365 Text: "malformed linter directive; missing the required reason field?", 366 Check: "", 367 Checker: "lint", 368 Package: nil, 369 } 370 out = append(out, p) 371 continue 372 } 373 default: 374 // unknown directive, ignore 375 continue 376 } 377 checks := strings.Split(args[0], ",") 378 pos := prog.DisplayPosition(node.Pos()) 379 var ig Ignore 380 switch cmd { 381 case "ignore": 382 ig = &LineIgnore{ 383 File: pos.Filename, 384 Line: pos.Line, 385 Checks: checks, 386 pos: c.Pos(), 387 } 388 case "file-ignore": 389 ig = &FileIgnore{ 390 File: pos.Filename, 391 Checks: checks, 392 } 393 } 394 l.automaticIgnores = append(l.automaticIgnores, ig) 395 } 396 } 397 } 398 } 399 } 400 401 sizes := struct { 402 types int 403 defs int 404 uses int 405 implicits int 406 selections int 407 scopes int 408 }{} 409 for _, pkg := range pkgs { 410 sizes.types += len(pkg.TypesInfo.Types) 411 sizes.defs += len(pkg.TypesInfo.Defs) 412 sizes.uses += len(pkg.TypesInfo.Uses) 413 sizes.implicits += len(pkg.TypesInfo.Implicits) 414 sizes.selections += len(pkg.TypesInfo.Selections) 415 sizes.scopes += len(pkg.TypesInfo.Scopes) 416 } 417 418 if stats != nil { 419 stats.OtherInitWork = time.Since(t) 420 } 421 422 for _, checker := range l.Checkers { 423 t := time.Now() 424 checker.Init(prog) 425 if stats != nil { 426 stats.CheckerInits[checker.Name()] = time.Since(t) 427 } 428 } 429 430 var jobs []*Job 431 var allChecks []string 432 433 for _, checker := range l.Checkers { 434 checks := checker.Checks() 435 for _, check := range checks { 436 allChecks = append(allChecks, check.ID) 437 j := &Job{ 438 Program: prog, 439 checker: checker.Name(), 440 check: check, 441 } 442 jobs = append(jobs, j) 443 } 444 } 445 446 max := len(jobs) 447 if l.MaxConcurrentJobs > 0 { 448 max = l.MaxConcurrentJobs 449 } 450 451 sem := make(chan struct{}, max) 452 wg := &sync.WaitGroup{} 453 for _, j := range jobs { 454 wg.Add(1) 455 go func(j *Job) { 456 defer func() { 457 if panicErr := recover(); panicErr != nil { 458 j.panicErr = fmt.Errorf("panic: %s: %s", panicErr, string(debug.Stack())) 459 } 460 }() 461 defer wg.Done() 462 sem <- struct{}{} 463 defer func() { <-sem }() 464 fn := j.check.Fn 465 if fn == nil { 466 return 467 } 468 t := time.Now() 469 fn(j) 470 j.duration = time.Since(t) 471 }(j) 472 } 473 wg.Wait() 474 475 for _, j := range jobs { 476 if j.panicErr != nil { 477 panic(j.panicErr) 478 } 479 480 if stats != nil { 481 stats.Jobs = append(stats.Jobs, JobStat{j.check.ID, j.duration}) 482 } 483 for _, p := range j.problems { 484 allowedChecks := FilterChecks(allChecks, p.Package.Config.Checks) 485 486 if l.ignore(p) { 487 p.Severity = Ignored 488 } 489 // TODO(dh): support globs in check white/blacklist 490 // OPT(dh): this approach doesn't actually disable checks, 491 // it just discards their results. For the moment, that's 492 // fine. None of our checks are super expensive. In the 493 // future, we may want to provide opt-in expensive 494 // analysis, which shouldn't run at all. It may be easiest 495 // to implement this in the individual checks. 496 if (l.ReturnIgnored || p.Severity != Ignored) && allowedChecks[p.Check] { 497 out = append(out, p) 498 } 499 } 500 } 501 502 for _, ig := range l.automaticIgnores { 503 ig, ok := ig.(*LineIgnore) 504 if !ok { 505 continue 506 } 507 if ig.matched { 508 continue 509 } 510 511 couldveMatched := false 512 for f, pkg := range prog.astFileMap { 513 if prog.Fset().Position(f.Pos()).Filename != ig.File { 514 continue 515 } 516 allowedChecks := FilterChecks(allChecks, pkg.Config.Checks) 517 for _, c := range ig.Checks { 518 if !allowedChecks[c] { 519 continue 520 } 521 couldveMatched = true 522 break 523 } 524 break 525 } 526 527 if !couldveMatched { 528 // The ignored checks were disabled for the containing package. 529 // Don't flag the ignore for not having matched. 530 continue 531 } 532 p := Problem{ 533 Position: prog.DisplayPosition(ig.pos), 534 Text: "this linter directive didn't match anything; should it be removed?", 535 Check: "", 536 Checker: "lint", 537 Package: nil, 538 } 539 out = append(out, p) 540 } 541 542 sort.Slice(out, func(i int, j int) bool { 543 pi, pj := out[i].Position, out[j].Position 544 545 if pi.Filename != pj.Filename { 546 return pi.Filename < pj.Filename 547 } 548 if pi.Line != pj.Line { 549 return pi.Line < pj.Line 550 } 551 if pi.Column != pj.Column { 552 return pi.Column < pj.Column 553 } 554 555 return out[i].Text < out[j].Text 556 }) 557 558 if l.PrintStats && stats != nil { 559 stats.Print(os.Stderr) 560 } 561 562 if len(out) < 2 { 563 return out 564 } 565 566 uniq := make([]Problem, 0, len(out)) 567 uniq = append(uniq, out[0]) 568 prev := out[0] 569 for _, p := range out[1:] { 570 if prev.Position == p.Position && prev.Text == p.Text { 571 continue 572 } 573 prev = p 574 uniq = append(uniq, p) 575 } 576 577 return uniq 578 } 579 580 func FilterChecks(allChecks []string, checks []string) map[string]bool { 581 // OPT(dh): this entire computation could be cached per package 582 allowedChecks := map[string]bool{} 583 584 for _, check := range checks { 585 b := true 586 if len(check) > 1 && check[0] == '-' { 587 b = false 588 check = check[1:] 589 } 590 if check == "*" || check == "all" { 591 // Match all 592 for _, c := range allChecks { 593 allowedChecks[c] = b 594 } 595 } else if strings.HasSuffix(check, "*") { 596 // Glob 597 prefix := check[:len(check)-1] 598 isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1 599 600 for _, c := range allChecks { 601 idx := strings.IndexFunc(c, func(r rune) bool { return unicode.IsNumber(r) }) 602 if isCat { 603 // Glob is S*, which should match S1000 but not SA1000 604 cat := c[:idx] 605 if prefix == cat { 606 allowedChecks[c] = b 607 } 608 } else { 609 // Glob is S1* 610 if strings.HasPrefix(c, prefix) { 611 allowedChecks[c] = b 612 } 613 } 614 } 615 } else { 616 // Literal check name 617 allowedChecks[check] = b 618 } 619 } 620 return allowedChecks 621 } 622 623 func (prog *Program) Package(path string) *packages.Package { 624 return prog.packagesMap[path] 625 } 626 627 // Pkg represents a package being linted. 628 type Pkg struct { 629 SSA *ssa.Package 630 *packages.Package 631 Config config.Config 632 } 633 634 type Positioner interface { 635 Pos() token.Pos 636 } 637 638 func (prog *Program) DisplayPosition(p token.Pos) token.Position { 639 // Only use the adjusted position if it points to another Go file. 640 // This means we'll point to the original file for cgo files, but 641 // we won't point to a YACC grammar file. 642 643 pos := prog.Fset().PositionFor(p, false) 644 adjPos := prog.Fset().PositionFor(p, true) 645 646 if filepath.Ext(adjPos.Filename) == ".go" { 647 return adjPos 648 } 649 return pos 650 } 651 652 func (prog *Program) isGenerated(path string) bool { 653 // This function isn't very efficient in terms of lock contention 654 // and lack of parallelism, but it really shouldn't matter. 655 // Projects consists of thousands of files, and have hundreds of 656 // errors. That's not a lot of calls to isGenerated. 657 658 prog.genMu.RLock() 659 if b, ok := prog.generatedMap[path]; ok { 660 prog.genMu.RUnlock() 661 return b 662 } 663 prog.genMu.RUnlock() 664 prog.genMu.Lock() 665 defer prog.genMu.Unlock() 666 // recheck to avoid doing extra work in case of race 667 if b, ok := prog.generatedMap[path]; ok { 668 return b 669 } 670 671 f, err := os.Open(path) 672 if err != nil { 673 return false 674 } 675 defer f.Close() 676 b := isGenerated(f) 677 prog.generatedMap[path] = b 678 return b 679 } 680 681 func (j *Job) Errorf(n Positioner, format string, args ...interface{}) *Problem { 682 tf := j.Program.SSA.Fset.File(n.Pos()) 683 f := j.Program.tokenFileMap[tf] 684 pkg := j.Program.astFileMap[f] 685 686 pos := j.Program.DisplayPosition(n.Pos()) 687 if j.Program.isGenerated(pos.Filename) && j.check.FilterGenerated { 688 return nil 689 } 690 problem := Problem{ 691 Position: pos, 692 Text: fmt.Sprintf(format, args...), 693 Check: j.check.ID, 694 Checker: j.checker, 695 Package: pkg, 696 } 697 j.problems = append(j.problems, problem) 698 return &j.problems[len(j.problems)-1] 699 } 700 701 func (j *Job) NodePackage(node Positioner) *Pkg { 702 f := j.File(node) 703 return j.Program.astFileMap[f] 704 } 705 706 func allPackages(pkgs []*packages.Package) []*packages.Package { 707 var out []*packages.Package 708 packages.Visit( 709 pkgs, 710 func(pkg *packages.Package) bool { 711 out = append(out, pkg) 712 return true 713 }, 714 nil, 715 ) 716 return out 717 }