+++ /dev/null
-// Package lint provides the foundation for tools like staticcheck
-package lint // import "honnef.co/go/tools/lint"
-
-import (
- "bytes"
- "encoding/gob"
- "fmt"
- "go/scanner"
- "go/token"
- "go/types"
- "path/filepath"
- "sort"
- "strings"
- "sync"
- "sync/atomic"
- "unicode"
-
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/packages"
- "honnef.co/go/tools/config"
- "honnef.co/go/tools/internal/cache"
-)
-
-type Documentation struct {
- Title string
- Text string
- Since string
- NonDefault bool
- Options []string
-}
-
-func (doc *Documentation) String() string {
- b := &strings.Builder{}
- fmt.Fprintf(b, "%s\n\n", doc.Title)
- if doc.Text != "" {
- fmt.Fprintf(b, "%s\n\n", doc.Text)
- }
- fmt.Fprint(b, "Available since\n ")
- if doc.Since == "" {
- fmt.Fprint(b, "unreleased")
- } else {
- fmt.Fprintf(b, "%s", doc.Since)
- }
- if doc.NonDefault {
- fmt.Fprint(b, ", non-default")
- }
- fmt.Fprint(b, "\n")
- if len(doc.Options) > 0 {
- fmt.Fprintf(b, "\nOptions\n")
- for _, opt := range doc.Options {
- fmt.Fprintf(b, " %s", opt)
- }
- fmt.Fprint(b, "\n")
- }
- return b.String()
-}
-
-type Ignore interface {
- Match(p Problem) bool
-}
-
-type LineIgnore struct {
- File string
- Line int
- Checks []string
- Matched bool
- Pos token.Position
-}
-
-func (li *LineIgnore) Match(p Problem) bool {
- pos := p.Pos
- if pos.Filename != li.File || pos.Line != li.Line {
- return false
- }
- for _, c := range li.Checks {
- if m, _ := filepath.Match(c, p.Check); m {
- li.Matched = true
- return true
- }
- }
- return false
-}
-
-func (li *LineIgnore) String() string {
- matched := "not matched"
- if li.Matched {
- matched = "matched"
- }
- return fmt.Sprintf("%s:%d %s (%s)", li.File, li.Line, strings.Join(li.Checks, ", "), matched)
-}
-
-type FileIgnore struct {
- File string
- Checks []string
-}
-
-func (fi *FileIgnore) Match(p Problem) bool {
- if p.Pos.Filename != fi.File {
- return false
- }
- for _, c := range fi.Checks {
- if m, _ := filepath.Match(c, p.Check); m {
- return true
- }
- }
- return false
-}
-
-type Severity uint8
-
-const (
- Error Severity = iota
- Warning
- Ignored
-)
-
-// Problem represents a problem in some source code.
-type Problem struct {
- Pos token.Position
- End token.Position
- Message string
- Check string
- Severity Severity
- Related []Related
-}
-
-type Related struct {
- Pos token.Position
- End token.Position
- Message string
-}
-
-func (p Problem) Equal(o Problem) bool {
- return p.Pos == o.Pos &&
- p.End == o.End &&
- p.Message == o.Message &&
- p.Check == o.Check &&
- p.Severity == o.Severity
-}
-
-func (p *Problem) String() string {
- return fmt.Sprintf("%s (%s)", p.Message, p.Check)
-}
-
-// A Linter lints Go source code.
-type Linter struct {
- Checkers []*analysis.Analyzer
- CumulativeCheckers []CumulativeChecker
- GoVersion int
- Config config.Config
- Stats Stats
- RepeatAnalyzers uint
-}
-
-type CumulativeChecker interface {
- Analyzer() *analysis.Analyzer
- Result() []types.Object
- ProblemObject(*token.FileSet, types.Object) Problem
-}
-
-func (l *Linter) Lint(cfg *packages.Config, patterns []string) ([]Problem, error) {
- var allAnalyzers []*analysis.Analyzer
- allAnalyzers = append(allAnalyzers, l.Checkers...)
- for _, cum := range l.CumulativeCheckers {
- allAnalyzers = append(allAnalyzers, cum.Analyzer())
- }
-
- // The -checks command line flag overrules all configuration
- // files, which means that for `-checks="foo"`, no check other
- // than foo can ever be reported to the user. Make use of this
- // fact to cull the list of analyses we need to run.
-
- // replace "inherit" with "all", as we don't want to base the
- // list of all checks on the default configuration, which
- // disables certain checks.
- checks := make([]string, len(l.Config.Checks))
- copy(checks, l.Config.Checks)
- for i, c := range checks {
- if c == "inherit" {
- checks[i] = "all"
- }
- }
-
- allowed := FilterChecks(allAnalyzers, checks)
- var allowedAnalyzers []*analysis.Analyzer
- for _, c := range l.Checkers {
- if allowed[c.Name] {
- allowedAnalyzers = append(allowedAnalyzers, c)
- }
- }
- hasCumulative := false
- for _, cum := range l.CumulativeCheckers {
- a := cum.Analyzer()
- if allowed[a.Name] {
- hasCumulative = true
- allowedAnalyzers = append(allowedAnalyzers, a)
- }
- }
-
- r, err := NewRunner(&l.Stats)
- if err != nil {
- return nil, err
- }
- r.goVersion = l.GoVersion
- r.repeatAnalyzers = l.RepeatAnalyzers
-
- pkgs, err := r.Run(cfg, patterns, allowedAnalyzers, hasCumulative)
- if err != nil {
- return nil, err
- }
-
- tpkgToPkg := map[*types.Package]*Package{}
- for _, pkg := range pkgs {
- tpkgToPkg[pkg.Types] = pkg
-
- for _, e := range pkg.errs {
- switch e := e.(type) {
- case types.Error:
- p := Problem{
- Pos: e.Fset.PositionFor(e.Pos, false),
- Message: e.Msg,
- Severity: Error,
- Check: "compile",
- }
- pkg.problems = append(pkg.problems, p)
- case packages.Error:
- msg := e.Msg
- if len(msg) != 0 && msg[0] == '\n' {
- // TODO(dh): See https://github.com/golang/go/issues/32363
- msg = msg[1:]
- }
-
- var pos token.Position
- if e.Pos == "" {
- // Under certain conditions (malformed package
- // declarations, multiple packages in the same
- // directory), go list emits an error on stderr
- // instead of JSON. Those errors do not have
- // associated position information in
- // go/packages.Error, even though the output on
- // stderr may contain it.
- if p, n, err := parsePos(msg); err == nil {
- if abs, err := filepath.Abs(p.Filename); err == nil {
- p.Filename = abs
- }
- pos = p
- msg = msg[n+2:]
- }
- } else {
- var err error
- pos, _, err = parsePos(e.Pos)
- if err != nil {
- panic(fmt.Sprintf("internal error: %s", e))
- }
- }
- p := Problem{
- Pos: pos,
- Message: msg,
- Severity: Error,
- Check: "compile",
- }
- pkg.problems = append(pkg.problems, p)
- case scanner.ErrorList:
- for _, e := range e {
- p := Problem{
- Pos: e.Pos,
- Message: e.Msg,
- Severity: Error,
- Check: "compile",
- }
- pkg.problems = append(pkg.problems, p)
- }
- case error:
- p := Problem{
- Pos: token.Position{},
- Message: e.Error(),
- Severity: Error,
- Check: "compile",
- }
- pkg.problems = append(pkg.problems, p)
- }
- }
- }
-
- atomic.StoreUint32(&r.stats.State, StateCumulative)
- for _, cum := range l.CumulativeCheckers {
- for _, res := range cum.Result() {
- pkg := tpkgToPkg[res.Pkg()]
- if pkg == nil {
- panic(fmt.Sprintf("analyzer %s flagged object %s in package %s, a package that we aren't tracking", cum.Analyzer(), res, res.Pkg()))
- }
- allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
- if allowedChecks[cum.Analyzer().Name] {
- pos := DisplayPosition(pkg.Fset, res.Pos())
- // FIXME(dh): why are we ignoring generated files
- // here? Surely this is specific to 'unused', not all
- // cumulative checkers
- if _, ok := pkg.gen[pos.Filename]; ok {
- continue
- }
- p := cum.ProblemObject(pkg.Fset, res)
- pkg.problems = append(pkg.problems, p)
- }
- }
- }
-
- for _, pkg := range pkgs {
- if !pkg.fromSource {
- // Don't cache packages that we loaded from the cache
- continue
- }
- cpkg := cachedPackage{
- Problems: pkg.problems,
- Ignores: pkg.ignores,
- Config: pkg.cfg,
- }
- buf := &bytes.Buffer{}
- if err := gob.NewEncoder(buf).Encode(cpkg); err != nil {
- return nil, err
- }
- id := cache.Subkey(pkg.actionID, "data "+r.problemsCacheKey)
- if err := r.cache.PutBytes(id, buf.Bytes()); err != nil {
- return nil, err
- }
- }
-
- var problems []Problem
- // Deduplicate line ignores. When U1000 processes a package and
- // its test variant, it will only emit a single problem for an
- // unused object, not two problems. We will, however, have two
- // line ignores, one per package. Without deduplication, one line
- // ignore will be marked as matched, while the other one won't,
- // subsequently reporting a "this linter directive didn't match
- // anything" error.
- ignores := map[token.Position]Ignore{}
- for _, pkg := range pkgs {
- for _, ig := range pkg.ignores {
- if lig, ok := ig.(*LineIgnore); ok {
- ig = ignores[lig.Pos]
- if ig == nil {
- ignores[lig.Pos] = lig
- ig = lig
- }
- }
- for i := range pkg.problems {
- p := &pkg.problems[i]
- if ig.Match(*p) {
- p.Severity = Ignored
- }
- }
- }
-
- if pkg.cfg == nil {
- // The package failed to load, otherwise we would have a
- // valid config. Pass through all errors.
- problems = append(problems, pkg.problems...)
- } else {
- for _, p := range pkg.problems {
- allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
- allowedChecks["compile"] = true
- if allowedChecks[p.Check] {
- problems = append(problems, p)
- }
- }
- }
-
- for _, ig := range pkg.ignores {
- ig, ok := ig.(*LineIgnore)
- if !ok {
- continue
- }
- ig = ignores[ig.Pos].(*LineIgnore)
- if ig.Matched {
- continue
- }
-
- couldveMatched := false
- allowedChecks := FilterChecks(allowedAnalyzers, pkg.cfg.Merge(l.Config).Checks)
- for _, c := range ig.Checks {
- if !allowedChecks[c] {
- continue
- }
- couldveMatched = true
- break
- }
-
- if !couldveMatched {
- // The ignored checks were disabled for the containing package.
- // Don't flag the ignore for not having matched.
- continue
- }
- p := Problem{
- Pos: ig.Pos,
- Message: "this linter directive didn't match anything; should it be removed?",
- Check: "",
- }
- problems = append(problems, p)
- }
- }
-
- if len(problems) == 0 {
- return nil, nil
- }
-
- sort.Slice(problems, func(i, j int) bool {
- pi := problems[i].Pos
- pj := problems[j].Pos
-
- if pi.Filename != pj.Filename {
- return pi.Filename < pj.Filename
- }
- if pi.Line != pj.Line {
- return pi.Line < pj.Line
- }
- if pi.Column != pj.Column {
- return pi.Column < pj.Column
- }
-
- return problems[i].Message < problems[j].Message
- })
-
- var out []Problem
- out = append(out, problems[0])
- for i, p := range problems[1:] {
- // We may encounter duplicate problems because one file
- // can be part of many packages.
- if !problems[i].Equal(p) {
- out = append(out, p)
- }
- }
- return out, nil
-}
-
-func FilterChecks(allChecks []*analysis.Analyzer, checks []string) map[string]bool {
- // OPT(dh): this entire computation could be cached per package
- allowedChecks := map[string]bool{}
-
- for _, check := range checks {
- b := true
- if len(check) > 1 && check[0] == '-' {
- b = false
- check = check[1:]
- }
- if check == "*" || check == "all" {
- // Match all
- for _, c := range allChecks {
- allowedChecks[c.Name] = b
- }
- } else if strings.HasSuffix(check, "*") {
- // Glob
- prefix := check[:len(check)-1]
- isCat := strings.IndexFunc(prefix, func(r rune) bool { return unicode.IsNumber(r) }) == -1
-
- for _, c := range allChecks {
- idx := strings.IndexFunc(c.Name, func(r rune) bool { return unicode.IsNumber(r) })
- if isCat {
- // Glob is S*, which should match S1000 but not SA1000
- cat := c.Name[:idx]
- if prefix == cat {
- allowedChecks[c.Name] = b
- }
- } else {
- // Glob is S1*
- if strings.HasPrefix(c.Name, prefix) {
- allowedChecks[c.Name] = b
- }
- }
- }
- } else {
- // Literal check name
- allowedChecks[check] = b
- }
- }
- return allowedChecks
-}
-
-func DisplayPosition(fset *token.FileSet, p token.Pos) token.Position {
- if p == token.NoPos {
- return token.Position{}
- }
-
- // Only use the adjusted position if it points to another Go file.
- // This means we'll point to the original file for cgo files, but
- // we won't point to a YACC grammar file.
- pos := fset.PositionFor(p, false)
- adjPos := fset.PositionFor(p, true)
-
- if filepath.Ext(adjPos.Filename) == ".go" {
- return adjPos
- }
- return pos
-}
-
-var bufferPool = &sync.Pool{
- New: func() interface{} {
- buf := bytes.NewBuffer(nil)
- buf.Grow(64)
- return buf
- },
-}
-
-func FuncName(f *types.Func) string {
- buf := bufferPool.Get().(*bytes.Buffer)
- buf.Reset()
- if f.Type() != nil {
- sig := f.Type().(*types.Signature)
- if recv := sig.Recv(); recv != nil {
- buf.WriteByte('(')
- if _, ok := recv.Type().(*types.Interface); ok {
- // gcimporter creates abstract methods of
- // named interfaces using the interface type
- // (not the named type) as the receiver.
- // Don't print it in full.
- buf.WriteString("interface")
- } else {
- types.WriteType(buf, recv.Type(), nil)
- }
- buf.WriteByte(')')
- buf.WriteByte('.')
- } else if f.Pkg() != nil {
- writePackage(buf, f.Pkg())
- }
- }
- buf.WriteString(f.Name())
- s := buf.String()
- bufferPool.Put(buf)
- return s
-}
-
-func writePackage(buf *bytes.Buffer, pkg *types.Package) {
- if pkg == nil {
- return
- }
- s := pkg.Path()
- if s != "" {
- buf.WriteString(s)
- buf.WriteByte('.')
- }
-}