add ability to set issue severity (#1155)

* 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
This commit is contained in:
Ryan Currah 2020-05-25 08:21:42 -04:00 committed by GitHub
parent dc260be693
commit fa7adcbda9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 691 additions and 225 deletions

2
.gitignore vendored
View file

@ -15,3 +15,5 @@
/.vscode/
*.test
.DS_Store
coverage.out
coverage.xml

View file

@ -382,3 +382,28 @@ issues:
# Show only new issues created in git patch with set file path.
new-from-patch: path/to/patch/file
severity:
# Default value is empty string.
# Set the default severity for issues. If severity rules are defined and the issues
# do not match or no severity is provided to the rule this will be the default
# severity applied. Severities should match the supported severity names of the
# selected out format.
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity
# - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
default-severity: error
# The default value is false.
# If set to true severity-rules regular expressions become case sensitive.
case-sensitive: false
# Default value is empty list.
# When a list of severity rules are provided, severity information will be added to lint
# issues. Severity rules have the same filtering capability as exclude rules except you
# are allowed to specify one matcher per severity rule.
# Only affects out formats that support setting severity information.
rules:
- linters:
- dupl
severity: info

1
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/fatih/color v1.9.0
github.com/go-critic/go-critic v0.4.3
github.com/go-lintpack/lintpack v0.5.2
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b
github.com/gofrs/flock v0.7.1
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a

View file

@ -407,13 +407,48 @@ type Linters struct {
Presets []string
}
type ExcludeRule struct {
type BaseRule struct {
Linters []string
Path string
Text string
Source string
}
func (b BaseRule) Validate(minConditionsCount int) error {
if err := validateOptionalRegex(b.Path); err != nil {
return fmt.Errorf("invalid path regex: %v", err)
}
if err := validateOptionalRegex(b.Text); err != nil {
return fmt.Errorf("invalid text regex: %v", err)
}
if err := validateOptionalRegex(b.Source); err != nil {
return fmt.Errorf("invalid source regex: %v", err)
}
nonBlank := 0
if len(b.Linters) > 0 {
nonBlank++
}
if b.Path != "" {
nonBlank++
}
if b.Text != "" {
nonBlank++
}
if b.Source != "" {
nonBlank++
}
if nonBlank < minConditionsCount {
return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount)
}
return nil
}
const excludeRuleMinConditionsCount = 2
type ExcludeRule struct {
BaseRule `mapstructure:",squash"`
}
func validateOptionalRegex(value string) error {
if value == "" {
return nil
@ -423,33 +458,18 @@ func validateOptionalRegex(value string) error {
}
func (e ExcludeRule) Validate() error {
if err := validateOptionalRegex(e.Path); err != nil {
return fmt.Errorf("invalid path regex: %v", err)
}
if err := validateOptionalRegex(e.Text); err != nil {
return fmt.Errorf("invalid text regex: %v", err)
}
if err := validateOptionalRegex(e.Source); err != nil {
return fmt.Errorf("invalid source regex: %v", err)
}
nonBlank := 0
if len(e.Linters) > 0 {
nonBlank++
}
if e.Path != "" {
nonBlank++
}
if e.Text != "" {
nonBlank++
}
if e.Source != "" {
nonBlank++
}
const minConditionsCount = 2
if nonBlank < minConditionsCount {
return errors.New("at least 2 of (text, source, path, linters) should be set")
}
return nil
return e.BaseRule.Validate(excludeRuleMinConditionsCount)
}
const severityRuleMinConditionsCount = 1
type SeverityRule struct {
BaseRule `mapstructure:",squash"`
Severity string
}
func (s *SeverityRule) Validate() error {
return s.BaseRule.Validate(severityRuleMinConditionsCount)
}
type Issues struct {
@ -469,6 +489,12 @@ type Issues struct {
NeedFix bool `mapstructure:"fix"`
}
type Severity struct {
Default string `mapstructure:"default-severity"`
CaseSensitive bool `mapstructure:"case-sensitive"`
Rules []SeverityRule `mapstructure:"rules"`
}
type Config struct {
Run Run
@ -484,6 +510,7 @@ type Config struct {
LintersSettings LintersSettings `mapstructure:"linters-settings"`
Linters Linters
Issues Issues
Severity Severity
InternalTest bool // Option is used only for testing golangci-lint code, don't use it
}

View file

@ -113,6 +113,14 @@ func (r *FileReader) validateConfig() error {
return fmt.Errorf("error in exclude rule #%d: %v", i, err)
}
}
if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
return errors.New("can't set severity rule option: no default severity defined")
}
for i, rule := range c.Severity.Rules {
if err := rule.Validate(); err != nil {
return fmt.Errorf("error in severity rule #%d: %v", i, err)
}
}
if err := c.LintersSettings.Govet.Validate(); err != nil {
return fmt.Errorf("error in govet config: %v", err)
}

View file

@ -31,24 +31,6 @@ type Runner struct {
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) {
icfg := cfg.Issues
excludePatterns := icfg.ExcludePatterns
if icfg.UseDefaultExcludes {
excludePatterns = append(excludePatterns, config.GetExcludePatternsStrings(icfg.IncludeDefaultExcludes)...)
}
var excludeTotalPattern string
if len(excludePatterns) != 0 {
excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(excludePatterns, "|"))
}
var excludeProcessor processors.Processor
if cfg.Issues.ExcludeCaseSensitive {
excludeProcessor = processors.NewExcludeCaseSensitive(excludeTotalPattern)
} else {
excludeProcessor = processors.NewExclude(excludeTotalPattern)
}
skipFilesProcessor, err := processors.NewSkipFiles(cfg.Run.SkipFiles)
if err != nil {
return nil, err
@ -63,22 +45,6 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint
return nil, err
}
var excludeRules []processors.ExcludeRule
for _, r := range icfg.ExcludeRules {
excludeRules = append(excludeRules, processors.ExcludeRule{
Text: r.Text,
Source: r.Source,
Path: r.Path,
Linters: r.Linters,
})
}
var excludeRulesProcessor processors.Processor
if cfg.Issues.ExcludeCaseSensitive {
excludeRulesProcessor = processors.NewExcludeRulesCaseSensitive(excludeRules, lineCache, log.Child("exclude_rules"))
} else {
excludeRulesProcessor = processors.NewExcludeRules(excludeRules, lineCache, log.Child("exclude_rules"))
}
enabledLinters, err := es.GetEnabledLintersMap()
if err != nil {
return nil, errors.Wrap(err, "failed to get enabled linters")
@ -101,17 +67,18 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint
// Must be before exclude because users see already marked output and configure excluding by it.
processors.NewIdentifierMarker(),
excludeProcessor,
excludeRulesProcessor,
getExcludeProcessor(&cfg.Issues),
getExcludeRulesProcessor(&cfg.Issues, log, lineCache),
processors.NewNolint(log.Child("nolint"), dbManager, enabledLinters),
processors.NewUniqByLine(cfg),
processors.NewDiff(icfg.Diff, icfg.DiffFromRevision, icfg.DiffPatchFilePath),
processors.NewDiff(cfg.Issues.Diff, cfg.Issues.DiffFromRevision, cfg.Issues.DiffPatchFilePath),
processors.NewMaxPerFileFromLinter(cfg),
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues"), cfg),
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), 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
@ -254,3 +221,89 @@ func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch, s
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
}

View file

@ -5,6 +5,8 @@ import (
"encoding/xml"
"fmt"
"github.com/go-xmlfmt/xmlfmt"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
@ -28,7 +30,7 @@ type checkstyleError struct {
Source string `xml:"source,attr"`
}
const defaultSeverity = "error"
const defaultCheckstyleSeverity = "error"
type Checkstyle struct{}
@ -54,12 +56,17 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
files[issue.FilePath()] = file
}
severity := defaultCheckstyleSeverity
if issue.Severity != "" {
severity = issue.Severity
}
newError := &checkstyleError{
Column: issue.Column(),
Line: issue.Line(),
Message: issue.Text,
Source: issue.FromLinter,
Severity: defaultSeverity,
Severity: severity,
}
file.Errors = append(file.Errors, newError)
@ -75,6 +82,6 @@ func (Checkstyle) Print(ctx context.Context, issues []result.Issue) error {
return err
}
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data)
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, xmlfmt.FormatXML(string(data), "", " "))
return nil
}

View file

@ -2,7 +2,6 @@ package printers
import (
"context"
"crypto/md5" //nolint:gosec
"encoding/json"
"fmt"
@ -14,6 +13,7 @@ import (
// It is just enough to support GitLab CI Code Quality - https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html
type CodeClimateIssue struct {
Description string `json:"description"`
Severity string `json:"severity,omitempty"`
Fingerprint string `json:"fingerprint"`
Location struct {
Path string `json:"path"`
@ -31,28 +31,23 @@ func NewCodeClimate() *CodeClimate {
}
func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error {
allIssues := []CodeClimateIssue{}
for ind := range issues {
i := &issues[ind]
var issue CodeClimateIssue
issue.Description = i.FromLinter + ": " + i.Text
issue.Location.Path = i.Pos.Filename
issue.Location.Lines.Begin = i.Pos.Line
codeClimateIssues := []CodeClimateIssue{}
for i := range issues {
issue := &issues[i]
codeClimateIssue := CodeClimateIssue{}
codeClimateIssue.Description = issue.Description()
codeClimateIssue.Location.Path = issue.Pos.Filename
codeClimateIssue.Location.Lines.Begin = issue.Pos.Line
codeClimateIssue.Fingerprint = issue.Fingerprint()
// Need a checksum of the issue, so we use MD5 of the filename, text, and first line of source if there is any
var firstLine string
if len(i.SourceLines) > 0 {
firstLine = i.SourceLines[0]
if issue.Severity != "" {
codeClimateIssue.Severity = issue.Severity
}
hash := md5.New() //nolint:gosec
_, _ = hash.Write([]byte(i.Pos.Filename + i.Text + firstLine))
issue.Fingerprint = fmt.Sprintf("%X", hash.Sum(nil))
allIssues = append(allIssues, issue)
codeClimateIssues = append(codeClimateIssues, codeClimateIssue)
}
outputJSON, err := json.Marshal(allIssues)
outputJSON, err := json.Marshal(codeClimateIssues)
if err != nil {
return err
}

View file

@ -11,6 +11,8 @@ import (
type github struct {
}
const defaultGithubSeverity = "error"
// Github output format outputs issues according to Github actions format:
// https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
func NewGithub() Printer {
@ -19,7 +21,12 @@ func NewGithub() Printer {
// print each line as: ::error file=app.js,line=10,col=15::Something went wrong
func formatIssueAsGithub(issue *result.Issue) string {
ret := fmt.Sprintf("::error file=%s,line=%d", issue.FilePath(), issue.Line())
severity := defaultGithubSeverity
if issue.Severity != "" {
severity = issue.Severity
}
ret := fmt.Sprintf("::%s file=%s,line=%d", severity, issue.FilePath(), issue.Line())
if issue.Pos.Column != 0 {
ret += fmt.Sprintf(",col=%d", issue.Pos.Column)
}

View file

@ -1,6 +1,8 @@
package result
import (
"crypto/md5" //nolint:gosec
"fmt"
"go/token"
"golang.org/x/tools/go/packages"
@ -26,6 +28,8 @@ type Issue struct {
FromLinter string
Text string
Severity string
// Source lines of a code with the issue to show
SourceLines []string
@ -76,3 +80,19 @@ func (i *Issue) GetLineRange() Range {
return *i.LineRange
}
func (i *Issue) Description() string {
return fmt.Sprintf("%s: %s", i.FromLinter, i.Text)
}
func (i *Issue) Fingerprint() string {
firstLine := ""
if len(i.SourceLines) > 0 {
firstLine = i.SourceLines[0]
}
hash := md5.New() //nolint:gosec
_, _ = hash.Write([]byte(fmt.Sprintf("%s%s%s", i.Pos.Filename, i.Text, firstLine)))
return fmt.Sprintf("%X", hash.Sum(nil))
}

View file

@ -0,0 +1,69 @@
package processors
import (
"regexp"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
type BaseRule struct {
Text string
Source string
Path string
Linters []string
}
type baseRule struct {
text *regexp.Regexp
source *regexp.Regexp
path *regexp.Regexp
linters []string
}
func (r *baseRule) isEmpty() bool {
return r.text == nil && r.source == nil && r.path == nil && len(r.linters) == 0
}
func (r *baseRule) match(issue *result.Issue, lineCache *fsutils.LineCache, log logutils.Log) bool {
if r.isEmpty() {
return false
}
if r.text != nil && !r.text.MatchString(issue.Text) {
return false
}
if r.path != nil && !r.path.MatchString(issue.FilePath()) {
return false
}
if len(r.linters) != 0 && !r.matchLinter(issue) {
return false
}
// the most heavyweight checking last
if r.source != nil && !r.matchSource(issue, lineCache, log) {
return false
}
return true
}
func (r *baseRule) matchLinter(issue *result.Issue) bool {
for _, linter := range r.linters {
if linter == issue.FromLinter {
return true
}
}
return false
}
func (r *baseRule) matchSource(issue *result.Issue, lineCache *fsutils.LineCache, log logutils.Log) bool { // nolint:interfacer
sourceLine, errSourceLine := lineCache.GetLine(issue.FilePath(), issue.Line())
if errSourceLine != nil {
log.Warnf("Failed to get line %s:%d from line cache: %s", issue.FilePath(), issue.Line(), errSourceLine)
return false // can't properly match
}
return r.source.MatchString(sourceLine)
}

View file

@ -9,21 +9,11 @@ import (
)
type excludeRule struct {
text *regexp.Regexp
source *regexp.Regexp
path *regexp.Regexp
linters []string
}
func (r *excludeRule) isEmpty() bool {
return r.text == nil && r.path == nil && len(r.linters) == 0
baseRule
}
type ExcludeRule struct {
Text string
Source string
Path string
Linters []string
BaseRule
}
type ExcludeRules struct {
@ -45,9 +35,8 @@ func NewExcludeRules(rules []ExcludeRule, lineCache *fsutils.LineCache, log logu
func createRules(rules []ExcludeRule, prefix string) []excludeRule {
parsedRules := make([]excludeRule, 0, len(rules))
for _, rule := range rules {
parsedRule := excludeRule{
linters: rule.Linters,
}
parsedRule := excludeRule{}
parsedRule.linters = rule.Linters
if rule.Text != "" {
parsedRule.text = regexp.MustCompile(prefix + rule.Text)
}
@ -69,7 +58,7 @@ func (p ExcludeRules) Process(issues []result.Issue) ([]result.Issue, error) {
return filterIssues(issues, func(i *result.Issue) bool {
for _, rule := range p.rules {
rule := rule
if p.match(i, &rule) {
if rule.match(i, p.lineCache, p.log) {
return false
}
}
@ -77,48 +66,6 @@ func (p ExcludeRules) Process(issues []result.Issue) ([]result.Issue, error) {
}), nil
}
func (p ExcludeRules) matchLinter(i *result.Issue, r *excludeRule) bool {
for _, linter := range r.linters {
if linter == i.FromLinter {
return true
}
}
return false
}
func (p ExcludeRules) matchSource(i *result.Issue, r *excludeRule) bool { //nolint:interfacer
sourceLine, err := p.lineCache.GetLine(i.FilePath(), i.Line())
if err != nil {
p.log.Warnf("Failed to get line %s:%d from line cache: %s", i.FilePath(), i.Line(), err)
return false // can't properly match
}
return r.source.MatchString(sourceLine)
}
func (p ExcludeRules) match(i *result.Issue, r *excludeRule) bool {
if r.isEmpty() {
return false
}
if r.text != nil && !r.text.MatchString(i.Text) {
return false
}
if r.path != nil && !r.path.MatchString(i.FilePath()) {
return false
}
if len(r.linters) != 0 && !p.matchLinter(i, r) {
return false
}
// the most heavyweight checking last
if r.source != nil && !p.matchSource(i, r) {
return false
}
return true
}
func (ExcludeRules) Name() string { return "exclude-rules" }
func (ExcludeRules) Finish() {}

View file

@ -1,7 +1,6 @@
package processors
import (
"go/token"
"path/filepath"
"testing"
@ -15,24 +14,32 @@ func TestExcludeRulesMultiple(t *testing.T) {
lineCache := fsutils.NewLineCache(fsutils.NewFileCache())
p := NewExcludeRules([]ExcludeRule{
{
Text: "^exclude$",
Linters: []string{"linter"},
BaseRule: BaseRule{
Text: "^exclude$",
Linters: []string{"linter"},
},
},
{
Linters: []string{"testlinter"},
Path: `_test\.go`,
BaseRule: BaseRule{
Linters: []string{"testlinter"},
Path: `_test\.go`,
},
},
{
Text: "^testonly$",
Path: `_test\.go`,
BaseRule: BaseRule{
Text: "^testonly$",
Path: `_test\.go`,
},
},
{
Source: "^//go:generate ",
Linters: []string{"lll"},
BaseRule: BaseRule{
Source: "^//go:generate ",
Linters: []string{"lll"},
},
},
}, lineCache, nil)
cases := []issueCase{
cases := []issueTestCase{
{Path: "e.go", Text: "exclude", Linter: "linter"},
{Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_test.go", Text: "normal", Linter: "testlinter"},
@ -43,19 +50,19 @@ func TestExcludeRulesMultiple(t *testing.T) {
}
var issues []result.Issue
for _, c := range cases {
issues = append(issues, newIssueCase(c))
issues = append(issues, newIssueFromIssueTestCase(c))
}
processedIssues := process(t, p, issues...)
var resultingCases []issueCase
var resultingCases []issueTestCase
for _, i := range processedIssues {
resultingCases = append(resultingCases, issueCase{
resultingCases = append(resultingCases, issueTestCase{
Path: i.FilePath(),
Linter: i.FromLinter,
Text: i.Text,
Line: i.Line(),
})
}
expectedCases := []issueCase{
expectedCases := []issueTestCase{
{Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_Test.go", Text: "normal", Linter: "testlinter"},
{Path: "e_test.go", Text: "another", Linter: "linter"},
@ -63,30 +70,12 @@ func TestExcludeRulesMultiple(t *testing.T) {
assert.Equal(t, expectedCases, resultingCases)
}
type issueCase struct {
Path string
Line int
Text string
Linter string
}
func newIssueCase(c issueCase) result.Issue {
return result.Issue{
Text: c.Text,
FromLinter: c.Linter,
Pos: token.Position{
Filename: c.Path,
Line: c.Line,
},
}
}
func TestExcludeRulesText(t *testing.T) {
p := NewExcludeRules([]ExcludeRule{
{
Text: "^exclude$",
Linters: []string{
"linter",
BaseRule: BaseRule{
Text: "^exclude$",
Linters: []string{"linter"},
},
},
}, nil, nil)
@ -110,31 +99,39 @@ func TestExcludeRulesText(t *testing.T) {
}
func TestExcludeRulesEmpty(t *testing.T) {
processAssertSame(t, NewExcludeRules(nil, nil, nil), newTextIssue("test"))
processAssertSame(t, NewExcludeRules(nil, nil, nil), newIssueFromTextTestCase("test"))
}
func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) {
lineCache := fsutils.NewLineCache(fsutils.NewFileCache())
p := NewExcludeRulesCaseSensitive([]ExcludeRule{
{
Text: "^exclude$",
Linters: []string{"linter"},
BaseRule: BaseRule{
Text: "^exclude$",
Linters: []string{"linter"},
},
},
{
Linters: []string{"testlinter"},
Path: `_test\.go`,
BaseRule: BaseRule{
Linters: []string{"testlinter"},
Path: `_test\.go`,
},
},
{
Text: "^testonly$",
Path: `_test\.go`,
BaseRule: BaseRule{
Text: "^testonly$",
Path: `_test\.go`,
},
},
{
Source: "^//go:generate ",
Linters: []string{"lll"},
BaseRule: BaseRule{
Source: "^//go:generate ",
Linters: []string{"lll"},
},
},
}, lineCache, nil)
cases := []issueCase{
cases := []issueTestCase{
{Path: "e.go", Text: "exclude", Linter: "linter"},
{Path: "e.go", Text: "excLude", Linter: "linter"},
{Path: "e.go", Text: "some", Linter: "linter"},
@ -147,19 +144,19 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) {
}
var issues []result.Issue
for _, c := range cases {
issues = append(issues, newIssueCase(c))
issues = append(issues, newIssueFromIssueTestCase(c))
}
processedIssues := process(t, p, issues...)
var resultingCases []issueCase
var resultingCases []issueTestCase
for _, i := range processedIssues {
resultingCases = append(resultingCases, issueCase{
resultingCases = append(resultingCases, issueTestCase{
Path: i.FilePath(),
Linter: i.FromLinter,
Text: i.Text,
Line: i.Line(),
})
}
expectedCases := []issueCase{
expectedCases := []issueTestCase{
{Path: "e.go", Text: "excLude", Linter: "linter"},
{Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_Test.go", Text: "normal", Linter: "testlinter"},
@ -173,9 +170,9 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) {
func TestExcludeRulesCaseSensitiveText(t *testing.T) {
p := NewExcludeRulesCaseSensitive([]ExcludeRule{
{
Text: "^exclude$",
Linters: []string{
"linter",
BaseRule: BaseRule{
Text: "^exclude$",
Linters: []string{"linter"},
},
},
}, nil, nil)
@ -199,5 +196,5 @@ func TestExcludeRulesCaseSensitiveText(t *testing.T) {
}
func TestExcludeRulesCaseSensitiveEmpty(t *testing.T) {
processAssertSame(t, NewExcludeRulesCaseSensitive(nil, nil, nil), newTextIssue("test"))
processAssertSame(t, NewExcludeRulesCaseSensitive(nil, nil, nil), newIssueFromTextTestCase("test"))
}

View file

@ -8,34 +8,12 @@ import (
"github.com/golangci/golangci-lint/pkg/result"
)
func newTextIssue(text string) result.Issue {
return result.Issue{
Text: text,
}
}
func process(t *testing.T, p Processor, issues ...result.Issue) []result.Issue {
processedIssues, err := p.Process(issues)
assert.NoError(t, err)
return processedIssues
}
func processAssertSame(t *testing.T, p Processor, issues ...result.Issue) {
processedIssues := process(t, p, issues...)
assert.Equal(t, issues, processedIssues)
}
func processAssertEmpty(t *testing.T, p Processor, issues ...result.Issue) {
processedIssues := process(t, p, issues...)
assert.Empty(t, processedIssues)
}
func TestExclude(t *testing.T) {
p := NewExclude("^exclude$")
texts := []string{"excLude", "1", "", "exclud", "notexclude"}
var issues []result.Issue
for _, t := range texts {
issues = append(issues, newTextIssue(t))
issues = append(issues, newIssueFromTextTestCase(t))
}
processedIssues := process(t, p, issues...)
@ -49,7 +27,7 @@ func TestExclude(t *testing.T) {
}
func TestNoExclude(t *testing.T) {
processAssertSame(t, NewExclude(""), newTextIssue("test"))
processAssertSame(t, NewExclude(""), newIssueFromTextTestCase("test"))
}
func TestExcludeCaseSensitive(t *testing.T) {
@ -57,7 +35,7 @@ func TestExcludeCaseSensitive(t *testing.T) {
texts := []string{"excLude", "1", "", "exclud", "exclude"}
var issues []result.Issue
for _, t := range texts {
issues = append(issues, newTextIssue(t))
issues = append(issues, newIssueFromTextTestCase(t))
}
processedIssues := process(t, p, issues...)

View file

@ -0,0 +1,51 @@
package processors
import (
"go/token"
"testing"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/stretchr/testify/assert"
)
type issueTestCase struct {
Path string
Line int
Text string
Linter string
Severity string
}
func newIssueFromIssueTestCase(c issueTestCase) result.Issue {
return result.Issue{
Text: c.Text,
FromLinter: c.Linter,
Pos: token.Position{
Filename: c.Path,
Line: c.Line,
},
}
}
func newIssueFromTextTestCase(text string) result.Issue {
return result.Issue{
Text: text,
}
}
func process(t *testing.T, p Processor, issues ...result.Issue) []result.Issue {
processedIssues, err := p.Process(issues)
assert.NoError(t, err)
return processedIssues
}
func processAssertSame(t *testing.T, p Processor, issues ...result.Issue) {
processedIssues := process(t, p, issues...)
assert.Equal(t, issues, processedIssues)
}
func processAssertEmpty(t *testing.T, p Processor, issues ...result.Issue) {
processedIssues := process(t, p, issues...)
assert.Empty(t, processedIssues)
}

View file

@ -0,0 +1,103 @@
package processors
import (
"regexp"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
)
type severityRule struct {
baseRule
severity string
}
type SeverityRule struct {
BaseRule
Severity string
}
type SeverityRules struct {
defaultSeverity string
rules []severityRule
lineCache *fsutils.LineCache
log logutils.Log
}
func NewSeverityRules(defaultSeverity string, rules []SeverityRule, lineCache *fsutils.LineCache, log logutils.Log) *SeverityRules {
r := &SeverityRules{
lineCache: lineCache,
log: log,
defaultSeverity: defaultSeverity,
}
r.rules = createSeverityRules(rules, "(?i)")
return r
}
func createSeverityRules(rules []SeverityRule, prefix string) []severityRule {
parsedRules := make([]severityRule, 0, len(rules))
for _, rule := range rules {
parsedRule := severityRule{}
parsedRule.linters = rule.Linters
parsedRule.severity = rule.Severity
if rule.Text != "" {
parsedRule.text = regexp.MustCompile(prefix + rule.Text)
}
if rule.Source != "" {
parsedRule.source = regexp.MustCompile(prefix + rule.Source)
}
if rule.Path != "" {
parsedRule.path = regexp.MustCompile(rule.Path)
}
parsedRules = append(parsedRules, parsedRule)
}
return parsedRules
}
func (p SeverityRules) Process(issues []result.Issue) ([]result.Issue, error) {
if len(p.rules) == 0 {
return issues, nil
}
return transformIssues(issues, func(i *result.Issue) *result.Issue {
for _, rule := range p.rules {
rule := rule
ruleSeverity := p.defaultSeverity
if rule.severity != "" {
ruleSeverity = rule.severity
}
if rule.match(i, p.lineCache, p.log) {
i.Severity = ruleSeverity
return i
}
}
i.Severity = p.defaultSeverity
return i
}), nil
}
func (SeverityRules) Name() string { return "severity-rules" }
func (SeverityRules) Finish() {}
var _ Processor = SeverityRules{}
type SeverityRulesCaseSensitive struct {
*SeverityRules
}
func NewSeverityRulesCaseSensitive(defaultSeverity string, rules []SeverityRule,
lineCache *fsutils.LineCache, log logutils.Log) *SeverityRulesCaseSensitive {
r := &SeverityRules{
lineCache: lineCache,
log: log,
defaultSeverity: defaultSeverity,
}
r.rules = createSeverityRules(rules, "")
return &SeverityRulesCaseSensitive{r}
}
func (SeverityRulesCaseSensitive) Name() string { return "severity-rules-case-sensitive" }

View file

@ -0,0 +1,173 @@
package processors
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/result"
)
func TestSeverityRulesMultiple(t *testing.T) {
lineCache := fsutils.NewLineCache(fsutils.NewFileCache())
log := report.NewLogWrapper(logutils.NewStderrLog(""), &report.Data{})
p := NewSeverityRules("error", []SeverityRule{
{
Severity: "info",
BaseRule: BaseRule{
Text: "^ssl$",
Linters: []string{"gosec"},
},
},
{
Severity: "info",
BaseRule: BaseRule{
Linters: []string{"linter"},
Path: "e.go",
},
},
{
Severity: "info",
BaseRule: BaseRule{
Text: "^testonly$",
Path: `_test\.go`,
},
},
{
BaseRule: BaseRule{
Source: "^//go:generate ",
Linters: []string{"lll"},
},
},
{
Severity: "info",
BaseRule: BaseRule{
Source: "^//go:dosomething",
},
},
{
Severity: "info",
BaseRule: BaseRule{
Linters: []string{"someotherlinter"},
},
},
{
Severity: "info",
BaseRule: BaseRule{
Linters: []string{"somelinter"},
},
},
{
Severity: "info",
},
}, lineCache, log)
cases := []issueTestCase{
{Path: "ssl.go", Text: "ssl", Linter: "gosec"},
{Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_test.go", Text: "testonly", Linter: "testlinter"},
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"},
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo"},
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter"},
{Path: "somenotmatchlinter.go", Text: "somenotmatchlinter", Linter: "somenotmatchlinter"},
{Path: "empty.go", Text: "empty", Linter: "empty"},
}
var issues []result.Issue
for _, c := range cases {
issues = append(issues, newIssueFromIssueTestCase(c))
}
processedIssues := process(t, p, issues...)
var resultingCases []issueTestCase
for _, i := range processedIssues {
resultingCases = append(resultingCases, issueTestCase{
Path: i.FilePath(),
Linter: i.FromLinter,
Text: i.Text,
Line: i.Line(),
Severity: i.Severity,
})
}
expectedCases := []issueTestCase{
{Path: "ssl.go", Text: "ssl", Linter: "gosec", Severity: "info"},
{Path: "e.go", Text: "some", Linter: "linter", Severity: "info"},
{Path: "e_test.go", Text: "testonly", Linter: "testlinter", Severity: "info"},
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll", Severity: "error"},
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo", Severity: "info"},
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter", Severity: "info"},
{Path: "somenotmatchlinter.go", Text: "somenotmatchlinter", Linter: "somenotmatchlinter", Severity: "error"},
{Path: "empty.go", Text: "empty", Linter: "empty", Severity: "error"},
}
assert.Equal(t, expectedCases, resultingCases)
}
func TestSeverityRulesText(t *testing.T) {
p := NewSeverityRules("", []SeverityRule{
{
BaseRule: BaseRule{
Text: "^severity$",
Linters: []string{"linter"},
},
},
}, nil, nil)
texts := []string{"seveRity", "1", "", "serverit", "notseverity"}
var issues []result.Issue
for _, t := range texts {
issues = append(issues, result.Issue{
Text: t,
FromLinter: "linter",
})
}
processedIssues := process(t, p, issues...)
assert.Len(t, processedIssues, len(issues))
var processedTexts []string
for _, i := range processedIssues {
processedTexts = append(processedTexts, i.Text)
}
assert.Equal(t, texts, processedTexts)
}
func TestSeverityRulesEmpty(t *testing.T) {
processAssertSame(t, NewSeverityRules("", nil, nil, nil), newIssueFromTextTestCase("test"))
}
func TestSeverityRulesCaseSensitive(t *testing.T) {
lineCache := fsutils.NewLineCache(fsutils.NewFileCache())
p := NewSeverityRulesCaseSensitive("error", []SeverityRule{
{
Severity: "info",
BaseRule: BaseRule{
Text: "^ssl$",
Linters: []string{"gosec", "someotherlinter"},
},
},
}, lineCache, nil)
cases := []issueTestCase{
{Path: "e.go", Text: "ssL", Linter: "gosec"},
}
var issues []result.Issue
for _, c := range cases {
issues = append(issues, newIssueFromIssueTestCase(c))
}
processedIssues := process(t, p, issues...)
var resultingCases []issueTestCase
for _, i := range processedIssues {
resultingCases = append(resultingCases, issueTestCase{
Path: i.FilePath(),
Linter: i.FromLinter,
Text: i.Text,
Line: i.Line(),
Severity: i.Severity,
})
}
expectedCases := []issueTestCase{
{Path: "e.go", Text: "ssL", Linter: "gosec", Severity: "error"},
}
assert.Equal(t, expectedCases, resultingCases)
}

View file

@ -0,0 +1,3 @@
package testdata
//go:dosomething