
* add ability to set issue severity for out formats that support it based on severity rules * fix lint issues * change log child name * code climate omit severity if empty * add tests for severity rules, add support for case sensitive rules, fix lint issues, better doc comments, share processor test * deduplicated rule logic into a base rule that can be used by multiple rule types, moved severity config to it's own parent key named severity, reduced size of NewRunner function to make it easier to read * put validate function under base rule struct * better validation error wording * add Fingerprint and Description methods to Issue struct, made codeclimate reporter easier to read, checkstyle output is now pretty printed
310 lines
8.5 KiB
Go
310 lines
8.5 KiB
Go
package lint
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/golangci/golangci-lint/internal/errorutil"
|
|
"github.com/golangci/golangci-lint/pkg/config"
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"github.com/golangci/golangci-lint/pkg/goutil"
|
|
"github.com/golangci/golangci-lint/pkg/lint/linter"
|
|
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
"github.com/golangci/golangci-lint/pkg/packages"
|
|
"github.com/golangci/golangci-lint/pkg/result"
|
|
"github.com/golangci/golangci-lint/pkg/result/processors"
|
|
"github.com/golangci/golangci-lint/pkg/timeutils"
|
|
|
|
gopackages "golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
type Runner struct {
|
|
Processors []processors.Processor
|
|
Log logutils.Log
|
|
}
|
|
|
|
func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lintersdb.EnabledSet,
|
|
lineCache *fsutils.LineCache, dbManager *lintersdb.Manager, pkgs []*gopackages.Package) (*Runner, error) {
|
|
skipFilesProcessor, err := processors.NewSkipFiles(cfg.Run.SkipFiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
skipDirs := cfg.Run.SkipDirs
|
|
if cfg.Run.UseDefaultSkipDirs {
|
|
skipDirs = append(skipDirs, packages.StdExcludeDirRegexps...)
|
|
}
|
|
skipDirsProcessor, err := processors.NewSkipDirs(skipDirs, log.Child("skip dirs"), cfg.Run.Args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
enabledLinters, err := es.GetEnabledLintersMap()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to get enabled linters")
|
|
}
|
|
|
|
return &Runner{
|
|
Processors: []processors.Processor{
|
|
processors.NewCgo(goenv),
|
|
|
|
// Must go after Cgo.
|
|
processors.NewFilenameUnadjuster(pkgs, log.Child("filename_unadjuster")),
|
|
|
|
// Must be before diff, nolint and exclude autogenerated processor at least.
|
|
processors.NewPathPrettifier(),
|
|
skipFilesProcessor,
|
|
skipDirsProcessor, // must be after path prettifier
|
|
|
|
processors.NewAutogeneratedExclude(),
|
|
|
|
// Must be before exclude because users see already marked output and configure excluding by it.
|
|
processors.NewIdentifierMarker(),
|
|
|
|
getExcludeProcessor(&cfg.Issues),
|
|
getExcludeRulesProcessor(&cfg.Issues, log, lineCache),
|
|
processors.NewNolint(log.Child("nolint"), dbManager, enabledLinters),
|
|
|
|
processors.NewUniqByLine(cfg),
|
|
processors.NewDiff(cfg.Issues.Diff, cfg.Issues.DiffFromRevision, cfg.Issues.DiffPatchFilePath),
|
|
processors.NewMaxPerFileFromLinter(cfg),
|
|
processors.NewMaxSameIssues(cfg.Issues.MaxSameIssues, log.Child("max_same_issues"), cfg),
|
|
processors.NewMaxFromLinter(cfg.Issues.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg),
|
|
processors.NewSourceCode(lineCache, log.Child("source_code")),
|
|
processors.NewPathShortener(),
|
|
getSeverityRulesProcessor(&cfg.Severity, log, lineCache),
|
|
},
|
|
Log: log,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Runner) runLinterSafe(ctx context.Context, lintCtx *linter.Context,
|
|
lc *linter.Config) (ret []result.Issue, err error) {
|
|
defer func() {
|
|
if panicData := recover(); panicData != nil {
|
|
if pe, ok := panicData.(*errorutil.PanicError); ok {
|
|
// Don't print stacktrace from goroutines twice
|
|
lintCtx.Log.Warnf("Panic: %s: %s", pe, pe.Stack())
|
|
} else {
|
|
err = fmt.Errorf("panic occurred: %s", panicData)
|
|
r.Log.Warnf("Panic stack trace: %s", debug.Stack())
|
|
}
|
|
}
|
|
}()
|
|
|
|
issues, err := lc.Linter.Run(ctx, lintCtx)
|
|
|
|
if lc.DoesChangeTypes {
|
|
// Packages in lintCtx might be dirty due to the last analysis,
|
|
// which affects to the next analysis.
|
|
// To avoid this issue, we clear type information from the packages.
|
|
// See https://github.com/golangci/golangci-lint/pull/944.
|
|
// Currently DoesChangeTypes is true only for `unused`.
|
|
lintCtx.ClearTypesInPackages()
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range issues {
|
|
if issues[i].FromLinter == "" {
|
|
issues[i].FromLinter = lc.Name()
|
|
}
|
|
}
|
|
|
|
return issues, nil
|
|
}
|
|
|
|
type processorStat struct {
|
|
inCount int
|
|
outCount int
|
|
}
|
|
|
|
func (r Runner) processLintResults(inIssues []result.Issue) []result.Issue {
|
|
sw := timeutils.NewStopwatch("processing", r.Log)
|
|
|
|
var issuesBefore, issuesAfter int
|
|
statPerProcessor := map[string]processorStat{}
|
|
|
|
var outIssues []result.Issue
|
|
if len(inIssues) != 0 {
|
|
issuesBefore += len(inIssues)
|
|
outIssues = r.processIssues(inIssues, sw, statPerProcessor)
|
|
issuesAfter += len(outIssues)
|
|
}
|
|
|
|
// finalize processors: logging, clearing, no heavy work here
|
|
|
|
for _, p := range r.Processors {
|
|
p := p
|
|
sw.TrackStage(p.Name(), func() {
|
|
p.Finish()
|
|
})
|
|
}
|
|
|
|
if issuesBefore != issuesAfter {
|
|
r.Log.Infof("Issues before processing: %d, after processing: %d", issuesBefore, issuesAfter)
|
|
}
|
|
r.printPerProcessorStat(statPerProcessor)
|
|
sw.PrintStages()
|
|
|
|
return outIssues
|
|
}
|
|
|
|
func (r Runner) printPerProcessorStat(stat map[string]processorStat) {
|
|
parts := make([]string, 0, len(stat))
|
|
for name, ps := range stat {
|
|
if ps.inCount != 0 {
|
|
parts = append(parts, fmt.Sprintf("%s: %d/%d", name, ps.outCount, ps.inCount))
|
|
}
|
|
}
|
|
if len(parts) != 0 {
|
|
r.Log.Infof("Processors filtering stat (out/in): %s", strings.Join(parts, ", "))
|
|
}
|
|
}
|
|
|
|
func (r Runner) Run(ctx context.Context, linters []*linter.Config, lintCtx *linter.Context) ([]result.Issue, error) {
|
|
sw := timeutils.NewStopwatch("linters", r.Log)
|
|
defer sw.Print()
|
|
|
|
var issues []result.Issue
|
|
var runErr error
|
|
for _, lc := range linters {
|
|
lc := lc
|
|
sw.TrackStage(lc.Name(), func() {
|
|
linterIssues, err := r.runLinterSafe(ctx, lintCtx, lc)
|
|
if err != nil {
|
|
r.Log.Warnf("Can't run linter %s: %s", lc.Linter.Name(), err)
|
|
if os.Getenv("GOLANGCI_COM_RUN") == "" {
|
|
// Don't stop all linters on one linter failure for golangci.com.
|
|
runErr = err
|
|
}
|
|
return
|
|
}
|
|
issues = append(issues, linterIssues...)
|
|
})
|
|
}
|
|
|
|
return r.processLintResults(issues), runErr
|
|
}
|
|
|
|
func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch, statPerProcessor map[string]processorStat) []result.Issue {
|
|
for _, p := range r.Processors {
|
|
var newIssues []result.Issue
|
|
var err error
|
|
p := p
|
|
sw.TrackStage(p.Name(), func() {
|
|
newIssues, err = p.Process(issues)
|
|
})
|
|
|
|
if err != nil {
|
|
r.Log.Warnf("Can't process result by %s processor: %s", p.Name(), err)
|
|
} else {
|
|
stat := statPerProcessor[p.Name()]
|
|
stat.inCount += len(issues)
|
|
stat.outCount += len(newIssues)
|
|
statPerProcessor[p.Name()] = stat
|
|
issues = newIssues
|
|
}
|
|
|
|
if issues == nil {
|
|
issues = []result.Issue{}
|
|
}
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
func getExcludeProcessor(cfg *config.Issues) processors.Processor {
|
|
excludePatterns := cfg.ExcludePatterns
|
|
if cfg.UseDefaultExcludes {
|
|
excludePatterns = append(excludePatterns, config.GetExcludePatternsStrings(cfg.IncludeDefaultExcludes)...)
|
|
}
|
|
|
|
var excludeTotalPattern string
|
|
if len(excludePatterns) != 0 {
|
|
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
|
|
}
|
|
|
|
var excludeProcessor processors.Processor
|
|
if cfg.ExcludeCaseSensitive {
|
|
excludeProcessor = processors.NewExcludeCaseSensitive(excludeTotalPattern)
|
|
} else {
|
|
excludeProcessor = processors.NewExclude(excludeTotalPattern)
|
|
}
|
|
|
|
return excludeProcessor
|
|
}
|
|
|
|
func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, lineCache *fsutils.LineCache) processors.Processor {
|
|
var excludeRules []processors.ExcludeRule
|
|
for _, r := range cfg.ExcludeRules {
|
|
excludeRules = append(excludeRules, processors.ExcludeRule{
|
|
BaseRule: processors.BaseRule{
|
|
Text: r.Text,
|
|
Source: r.Source,
|
|
Path: r.Path,
|
|
Linters: r.Linters,
|
|
},
|
|
})
|
|
}
|
|
|
|
var excludeRulesProcessor processors.Processor
|
|
if cfg.ExcludeCaseSensitive {
|
|
excludeRulesProcessor = processors.NewExcludeRulesCaseSensitive(
|
|
excludeRules,
|
|
lineCache,
|
|
log.Child("exclude_rules"),
|
|
)
|
|
} else {
|
|
excludeRulesProcessor = processors.NewExcludeRules(
|
|
excludeRules,
|
|
lineCache,
|
|
log.Child("exclude_rules"),
|
|
)
|
|
}
|
|
|
|
return excludeRulesProcessor
|
|
}
|
|
|
|
func getSeverityRulesProcessor(cfg *config.Severity, log logutils.Log, lineCache *fsutils.LineCache) processors.Processor {
|
|
var severityRules []processors.SeverityRule
|
|
for _, r := range cfg.Rules {
|
|
severityRules = append(severityRules, processors.SeverityRule{
|
|
Severity: r.Severity,
|
|
BaseRule: processors.BaseRule{
|
|
Text: r.Text,
|
|
Source: r.Source,
|
|
Path: r.Path,
|
|
Linters: r.Linters,
|
|
},
|
|
})
|
|
}
|
|
|
|
var severityRulesProcessor processors.Processor
|
|
if cfg.CaseSensitive {
|
|
severityRulesProcessor = processors.NewSeverityRulesCaseSensitive(
|
|
cfg.Default,
|
|
severityRules,
|
|
lineCache,
|
|
log.Child("severity_rules"),
|
|
)
|
|
} else {
|
|
severityRulesProcessor = processors.NewSeverityRules(
|
|
cfg.Default,
|
|
severityRules,
|
|
lineCache,
|
|
log.Child("severity_rules"),
|
|
)
|
|
}
|
|
|
|
return severityRulesProcessor
|
|
}
|