staticcheck (#313)
* 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>
This commit is contained in:
committed by
GitHub
parent
93f46e03ea
commit
50fbc2eec2
577
vendor/honnef.co/go/tools/lintcmd/lint.go
vendored
Normal file
577
vendor/honnef.co/go/tools/lintcmd/lint.go
vendored
Normal file
@@ -0,0 +1,577 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user