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  }