| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- package lintcmd
- import (
- "crypto/sha256"
- "fmt"
- "go/build"
- "go/token"
- "io"
- "os"
- "os/signal"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "time"
- "unicode"
- "honnef.co/go/tools/analysis/lint"
- "honnef.co/go/tools/config"
- "honnef.co/go/tools/go/buildid"
- "honnef.co/go/tools/go/loader"
- "honnef.co/go/tools/lintcmd/cache"
- "honnef.co/go/tools/lintcmd/runner"
- "honnef.co/go/tools/unused"
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/packages"
- )
- // A linter lints Go source code.
- type linter struct {
- Analyzers map[string]*lint.Analyzer
- Runner *runner.Runner
- }
- func computeSalt() ([]byte, error) {
- p, err := os.Executable()
- if err != nil {
- return nil, err
- }
- if id, err := buildid.ReadFile(p); err == nil {
- return []byte(id), nil
- } else {
- // For some reason we couldn't read the build id from the executable.
- // Fall back to hashing the entire executable.
- f, err := os.Open(p)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- h := sha256.New()
- if _, err := io.Copy(h, f); err != nil {
- return nil, err
- }
- return h.Sum(nil), nil
- }
- }
- func newLinter(cfg config.Config) (*linter, error) {
- c, err := cache.Default()
- if err != nil {
- return nil, err
- }
- salt, err := computeSalt()
- if err != nil {
- return nil, fmt.Errorf("could not compute salt for cache: %s", err)
- }
- c.SetSalt(salt)
- r, err := runner.New(cfg, c)
- if err != nil {
- return nil, err
- }
- r.FallbackGoVersion = defaultGoVersion()
- return &linter{
- Runner: r,
- }, nil
- }
- type LintResult struct {
- CheckedFiles []string
- Diagnostics []diagnostic
- Warnings []string
- }
- func (l *linter) Lint(cfg *packages.Config, patterns []string) (LintResult, error) {
- var out LintResult
- as := make([]*analysis.Analyzer, 0, len(l.Analyzers))
- for _, a := range l.Analyzers {
- as = append(as, a.Analyzer)
- }
- results, err := l.Runner.Run(cfg, as, patterns)
- if err != nil {
- return out, err
- }
- if len(results) == 0 {
- // TODO(dh): emulate Go's behavior more closely once we have
- // access to go list's Match field.
- for _, pattern := range patterns {
- fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
- }
- }
- analyzerNames := make([]string, 0, len(l.Analyzers))
- for name := range l.Analyzers {
- analyzerNames = append(analyzerNames, name)
- }
- used := map[unusedKey]bool{}
- var unuseds []unusedPair
- for _, res := range results {
- if len(res.Errors) > 0 && !res.Failed {
- panic("package has errors but isn't marked as failed")
- }
- if res.Failed {
- out.Diagnostics = append(out.Diagnostics, failed(res)...)
- } else {
- if res.Skipped {
- out.Warnings = append(out.Warnings, fmt.Sprintf("skipped package %s because it is too large", res.Package))
- continue
- }
- if !res.Initial {
- continue
- }
- out.CheckedFiles = append(out.CheckedFiles, res.Package.GoFiles...)
- allowedAnalyzers := filterAnalyzerNames(analyzerNames, res.Config.Checks)
- resd, err := res.Load()
- if err != nil {
- return out, err
- }
- ps := success(allowedAnalyzers, resd)
- filtered, err := filterIgnored(ps, resd, allowedAnalyzers)
- if err != nil {
- return out, err
- }
- // OPT move this code into the 'success' function.
- for i, diag := range filtered {
- a := l.Analyzers[diag.Category]
- // Some diag.Category don't map to analyzers, such as "staticcheck"
- if a != nil {
- filtered[i].MergeIf = a.Doc.MergeIf
- }
- }
- out.Diagnostics = append(out.Diagnostics, filtered...)
- for _, obj := range resd.Unused.Used {
- // FIXME(dh): pick the object whose filename does not include $GOROOT
- key := unusedKey{
- pkgPath: res.Package.PkgPath,
- base: filepath.Base(obj.Position.Filename),
- line: obj.Position.Line,
- name: obj.Name,
- }
- used[key] = true
- }
- if allowedAnalyzers["U1000"] {
- for _, obj := range resd.Unused.Unused {
- key := unusedKey{
- pkgPath: res.Package.PkgPath,
- base: filepath.Base(obj.Position.Filename),
- line: obj.Position.Line,
- name: obj.Name,
- }
- unuseds = append(unuseds, unusedPair{key, obj})
- if _, ok := used[key]; !ok {
- used[key] = false
- }
- }
- }
- }
- }
- for _, uo := range unuseds {
- if uo.obj.Kind == "type param" {
- // We don't currently flag unused type parameters on used objects, and flagging them on unused objects isn't
- // useful.
- continue
- }
- if used[uo.key] {
- continue
- }
- if uo.obj.InGenerated {
- continue
- }
- out.Diagnostics = append(out.Diagnostics, diagnostic{
- Diagnostic: runner.Diagnostic{
- Position: uo.obj.DisplayPosition,
- Message: fmt.Sprintf("%s %s is unused", uo.obj.Kind, uo.obj.Name),
- Category: "U1000",
- },
- MergeIf: lint.MergeIfAll,
- })
- }
- return out, nil
- }
- func filterIgnored(diagnostics []diagnostic, res runner.ResultData, allowedAnalyzers map[string]bool) ([]diagnostic, error) {
- couldHaveMatched := func(ig *lineIgnore) bool {
- for _, c := range ig.Checks {
- if c == "U1000" {
- // We never want to flag ignores for U1000,
- // because U1000 isn't local to a single
- // package. For example, an identifier may
- // only be used by tests, in which case an
- // ignore would only fire when not analyzing
- // tests. To avoid spurious "useless ignore"
- // warnings, just never flag U1000.
- return false
- }
- // Even though the runner always runs all analyzers, we
- // still only flag unmatched ignores for the set of
- // analyzers the user has expressed interest in. That way,
- // `staticcheck -checks=SA1000` won't complain about an
- // unmatched ignore for an unrelated check.
- if allowedAnalyzers[c] {
- return true
- }
- }
- return false
- }
- ignores, moreDiagnostics := parseDirectives(res.Directives)
- for _, ig := range ignores {
- for i := range diagnostics {
- diag := &diagnostics[i]
- if ig.Match(*diag) {
- diag.Severity = severityIgnored
- }
- }
- if ig, ok := ig.(*lineIgnore); ok && !ig.Matched && couldHaveMatched(ig) {
- diag := diagnostic{
- Diagnostic: runner.Diagnostic{
- Position: ig.Pos,
- Message: "this linter directive didn't match anything; should it be removed?",
- Category: "staticcheck",
- },
- }
- moreDiagnostics = append(moreDiagnostics, diag)
- }
- }
- return append(diagnostics, moreDiagnostics...), nil
- }
- type ignore interface {
- Match(diag diagnostic) bool
- }
- type lineIgnore struct {
- File string
- Line int
- Checks []string
- Matched bool
- Pos token.Position
- }
- func (li *lineIgnore) Match(p diagnostic) bool {
- pos := p.Position
- if pos.Filename != li.File || pos.Line != li.Line {
- return false
- }
- for _, c := range li.Checks {
- if m, _ := filepath.Match(c, p.Category); 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 diagnostic) bool {
- if p.Position.Filename != fi.File {
- return false
- }
- for _, c := range fi.Checks {
- if m, _ := filepath.Match(c, p.Category); m {
- return true
- }
- }
- return false
- }
- type severity uint8
- const (
- severityError severity = iota
- severityWarning
- severityIgnored
- )
- func (s severity) String() string {
- switch s {
- case severityError:
- return "error"
- case severityWarning:
- return "warning"
- case severityIgnored:
- return "ignored"
- default:
- return fmt.Sprintf("Severity(%d)", s)
- }
- }
- // diagnostic represents a diagnostic in some source code.
- type diagnostic struct {
- runner.Diagnostic
- Severity severity
- MergeIf lint.MergeStrategy
- BuildName string
- }
- func (p diagnostic) equal(o diagnostic) bool {
- return p.Position == o.Position &&
- p.End == o.End &&
- p.Message == o.Message &&
- p.Category == o.Category &&
- p.Severity == o.Severity &&
- p.MergeIf == o.MergeIf &&
- p.BuildName == o.BuildName
- }
- func (p *diagnostic) String() string {
- if p.BuildName != "" {
- return fmt.Sprintf("%s [%s] (%s)", p.Message, p.BuildName, p.Category)
- } else {
- return fmt.Sprintf("%s (%s)", p.Message, p.Category)
- }
- }
- func failed(res runner.Result) []diagnostic {
- var diagnostics []diagnostic
- for _, e := range res.Errors {
- switch e := e.(type) {
- 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 posn 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
- }
- posn = p
- msg = msg[n+2:]
- }
- } else {
- var err error
- posn, _, err = parsePos(e.Pos)
- if err != nil {
- panic(fmt.Sprintf("internal error: %s", e))
- }
- }
- diag := diagnostic{
- Diagnostic: runner.Diagnostic{
- Position: posn,
- Message: msg,
- Category: "compile",
- },
- Severity: severityError,
- }
- diagnostics = append(diagnostics, diag)
- case error:
- diag := diagnostic{
- Diagnostic: runner.Diagnostic{
- Position: token.Position{},
- Message: e.Error(),
- Category: "compile",
- },
- Severity: severityError,
- }
- diagnostics = append(diagnostics, diag)
- }
- }
- return diagnostics
- }
- type unusedKey struct {
- pkgPath string
- base string
- line int
- name string
- }
- type unusedPair struct {
- key unusedKey
- obj unused.SerializedObject
- }
- func success(allowedAnalyzers map[string]bool, res runner.ResultData) []diagnostic {
- diags := res.Diagnostics
- var diagnostics []diagnostic
- for _, diag := range diags {
- if !allowedAnalyzers[diag.Category] {
- continue
- }
- diagnostics = append(diagnostics, diagnostic{Diagnostic: diag})
- }
- return diagnostics
- }
- func defaultGoVersion() string {
- tags := build.Default.ReleaseTags
- v := tags[len(tags)-1][2:]
- return v
- }
- func filterAnalyzerNames(analyzers []string, checks []string) map[string]bool {
- 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 analyzers {
- allowedChecks[c] = 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 _, a := range analyzers {
- idx := strings.IndexFunc(a, func(r rune) bool { return unicode.IsNumber(r) })
- if isCat {
- // Glob is S*, which should match S1000 but not SA1000
- cat := a[:idx]
- if prefix == cat {
- allowedChecks[a] = b
- }
- } else {
- // Glob is S1*
- if strings.HasPrefix(a, prefix) {
- allowedChecks[a] = b
- }
- }
- }
- } else {
- // Literal check name
- allowedChecks[check] = b
- }
- }
- return allowedChecks
- }
- var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
- func parsePos(pos string) (token.Position, int, error) {
- if pos == "-" || pos == "" {
- return token.Position{}, 0, nil
- }
- parts := posRe.FindStringSubmatch(pos)
- if parts == nil {
- return token.Position{}, 0, fmt.Errorf("internal error: malformed position %q", pos)
- }
- file := parts[1]
- line, _ := strconv.Atoi(parts[2])
- col, _ := strconv.Atoi(parts[3])
- return token.Position{
- Filename: file,
- Line: line,
- Column: col,
- }, len(parts[0]), nil
- }
- type options struct {
- Config config.Config
- BuildConfig BuildConfig
- LintTests bool
- GoVersion string
- PrintAnalyzerMeasurement func(analysis *analysis.Analyzer, pkg *loader.PackageSpec, d time.Duration)
- }
- func doLint(as []*lint.Analyzer, paths []string, opt *options) (LintResult, error) {
- if opt == nil {
- opt = &options{}
- }
- l, err := newLinter(opt.Config)
- if err != nil {
- return LintResult{}, err
- }
- analyzers := make(map[string]*lint.Analyzer, len(as))
- for _, a := range as {
- analyzers[a.Analyzer.Name] = a
- }
- l.Analyzers = analyzers
- l.Runner.GoVersion = opt.GoVersion
- l.Runner.Stats.PrintAnalyzerMeasurement = opt.PrintAnalyzerMeasurement
- cfg := &packages.Config{}
- if opt.LintTests {
- cfg.Tests = true
- }
- cfg.BuildFlags = opt.BuildConfig.Flags
- cfg.Env = append(os.Environ(), opt.BuildConfig.Envs...)
- printStats := func() {
- // Individual stats are read atomically, but overall there
- // is no synchronisation. For printing rough progress
- // information, this doesn't matter.
- switch l.Runner.Stats.State() {
- case runner.StateInitializing:
- fmt.Fprintln(os.Stderr, "Status: initializing")
- case runner.StateLoadPackageGraph:
- fmt.Fprintln(os.Stderr, "Status: loading package graph")
- case runner.StateBuildActionGraph:
- fmt.Fprintln(os.Stderr, "Status: building action graph")
- case runner.StateProcessing:
- fmt.Fprintf(os.Stderr, "Packages: %d/%d initial, %d/%d total; Workers: %d/%d\n",
- l.Runner.Stats.ProcessedInitialPackages(),
- l.Runner.Stats.InitialPackages(),
- l.Runner.Stats.ProcessedPackages(),
- l.Runner.Stats.TotalPackages(),
- l.Runner.ActiveWorkers(),
- l.Runner.TotalWorkers(),
- )
- case runner.StateFinalizing:
- fmt.Fprintln(os.Stderr, "Status: finalizing")
- }
- }
- if len(infoSignals) > 0 {
- ch := make(chan os.Signal, 1)
- signal.Notify(ch, infoSignals...)
- defer signal.Stop(ch)
- go func() {
- for range ch {
- printStats()
- }
- }()
- }
- res, err := l.Lint(cfg, paths)
- for i := range res.Diagnostics {
- res.Diagnostics[i].BuildName = opt.BuildConfig.Name
- }
- return res, err
- }
|