50fbc2eec2
* CI: use staticcheck for linting This commit switches the linter for Go code from golint to staticcheck. Golint has been deprecated since last year and staticcheck is a recommended replacement. Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * revendor Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * cmd,pkg: fix lint warnings Signed-off-by: Lucas Servén Marín <lserven@gmail.com>
578 lines
14 KiB
Go
578 lines
14 KiB
Go
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
|
|
}
|