From e1d5b240530096364be4185e49181111d6ee1906 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Mon, 18 May 2020 17:22:34 -0400 Subject: [PATCH 1/9] add ability to set issue severity for out formats that support it based on severity rules --- .golangci.example.yml | 16 +++ pkg/config/config.go | 41 +++++++ pkg/config/reader.go | 8 ++ pkg/lint/runner.go | 12 +++ pkg/printers/checkstyle.go | 9 +- pkg/printers/codeclimate.go | 5 + pkg/printers/github.go | 9 +- pkg/result/issue.go | 2 + pkg/result/processors/exclude_rules.go | 2 +- pkg/result/processors/severity_rules.go | 136 ++++++++++++++++++++++++ 10 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 pkg/result/processors/severity_rules.go diff --git a/.golangci.example.yml b/.golangci.example.yml index 4f9cdb5cb88f..f0fba94cd626 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -382,3 +382,19 @@ issues: # Show only new issues created in git patch with set file path. new-from-patch: path/to/patch/file + + # 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 + severity-default: error + + # When a list of severity rules are provided severity information will be added to lint issues. + # Severity rules has the same filtering capability as exclude rules. + # Only affects out formats that support setting severity information. + severity-rules: + - linters: + - dupl + severity: info diff --git a/pkg/config/config.go b/pkg/config/config.go index c9175fc63897..b7d692a0b7aa 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -452,6 +452,44 @@ func (e ExcludeRule) Validate() error { return nil } +type SeverityRule struct { + Severity string + Linters []string + Path string + Text string + Source string +} + +func (s *SeverityRule) Validate() error { + if err := validateOptionalRegex(s.Path); err != nil { + return fmt.Errorf("invalid path regex: %v", err) + } + if err := validateOptionalRegex(s.Text); err != nil { + return fmt.Errorf("invalid text regex: %v", err) + } + if err := validateOptionalRegex(s.Source); err != nil { + return fmt.Errorf("invalid source regex: %v", err) + } + nonBlank := 0 + if len(s.Linters) > 0 { + nonBlank++ + } + if s.Path != "" { + nonBlank++ + } + if s.Text != "" { + nonBlank++ + } + if s.Source != "" { + nonBlank++ + } + const minConditionsCount = 1 + if nonBlank < minConditionsCount { + return errors.New("at least 1 of (text, source, path, linters) should be set") + } + return nil +} + type Issues struct { IncludeDefaultExcludes []string `mapstructure:"include"` ExcludeCaseSensitive bool `mapstructure:"exclude-case-sensitive"` @@ -459,6 +497,9 @@ type Issues struct { ExcludeRules []ExcludeRule `mapstructure:"exclude-rules"` UseDefaultExcludes bool `mapstructure:"exclude-use-default"` + SeverityDefault string `mapstructure:"severity-default"` + SeverityRules []SeverityRule `mapstructure:"severity-rules"` + MaxIssuesPerLinter int `mapstructure:"max-issues-per-linter"` MaxSameIssues int `mapstructure:"max-same-issues"` diff --git a/pkg/config/reader.go b/pkg/config/reader.go index 1e355e72213a..94429403a592 100644 --- a/pkg/config/reader.go +++ b/pkg/config/reader.go @@ -113,6 +113,14 @@ func (r *FileReader) validateConfig() error { return fmt.Errorf("error in exclude rule #%d: %v", i, err) } } + if len(c.Issues.SeverityRules) > 0 && c.Issues.SeverityDefault == "" { + return errors.New("can't set severity rule option: no severity default defined") + } + for i, rule := range c.Issues.SeverityRules { + 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) } diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 05dc51ba9d74..144742cdd792 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -84,6 +84,17 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint return nil, errors.Wrap(err, "failed to get enabled linters") } + var severityRules []processors.SeverityRule + for _, r := range icfg.SeverityRules { + severityRules = append(severityRules, processors.SeverityRule{ + Severity: r.Severity, + Text: r.Text, + Source: r.Source, + Path: r.Path, + Linters: r.Linters, + }) + } + return &Runner{ Processors: []processors.Processor{ processors.NewCgo(goenv), @@ -112,6 +123,7 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg), processors.NewSourceCode(lineCache, log.Child("source_code")), processors.NewPathShortener(), + processors.NewSeverityRules(icfg.SeverityDefault, severityRules, lineCache, log.Child("exclude_rules")), }, Log: log, }, nil diff --git a/pkg/printers/checkstyle.go b/pkg/printers/checkstyle.go index f36bc108adc1..b3673dc2ab13 100644 --- a/pkg/printers/checkstyle.go +++ b/pkg/printers/checkstyle.go @@ -28,7 +28,7 @@ type checkstyleError struct { Source string `xml:"source,attr"` } -const defaultSeverity = "error" +const defaultCheckstyleSeverity = "error" type Checkstyle struct{} @@ -54,12 +54,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) diff --git a/pkg/printers/codeclimate.go b/pkg/printers/codeclimate.go index 5d45c4eb3019..980c51de44ab 100644 --- a/pkg/printers/codeclimate.go +++ b/pkg/printers/codeclimate.go @@ -14,6 +14,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"` Fingerprint string `json:"fingerprint"` Location struct { Path string `json:"path"` @@ -39,6 +40,10 @@ func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error { issue.Location.Path = i.Pos.Filename issue.Location.Lines.Begin = i.Pos.Line + if i.Severity != "" { + issue.Severity = i.Severity + } + // 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 { diff --git a/pkg/printers/github.go b/pkg/printers/github.go index fa11a2839a33..b8d70140a80c 100644 --- a/pkg/printers/github.go +++ b/pkg/printers/github.go @@ -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) } diff --git a/pkg/result/issue.go b/pkg/result/issue.go index 16d9a8a8c19d..d5a8f24c6dc9 100644 --- a/pkg/result/issue.go +++ b/pkg/result/issue.go @@ -26,6 +26,8 @@ type Issue struct { FromLinter string Text string + Severity string + // Source lines of a code with the issue to show SourceLines []string diff --git a/pkg/result/processors/exclude_rules.go b/pkg/result/processors/exclude_rules.go index b926af5b1d47..0d5a9c5698eb 100644 --- a/pkg/result/processors/exclude_rules.go +++ b/pkg/result/processors/exclude_rules.go @@ -97,7 +97,7 @@ func (p ExcludeRules) matchSource(i *result.Issue, r *excludeRule) bool { //noli return r.source.MatchString(sourceLine) } -func (p ExcludeRules) match(i *result.Issue, r *excludeRule) bool { +func (p ExcludeRules) match(i *result.Issue, r *excludeRule) bool { //nolint:dupl if r.isEmpty() { return false } diff --git a/pkg/result/processors/severity_rules.go b/pkg/result/processors/severity_rules.go new file mode 100644 index 000000000000..2ffc44c6a8bb --- /dev/null +++ b/pkg/result/processors/severity_rules.go @@ -0,0 +1,136 @@ +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 { + severity string + text *regexp.Regexp + source *regexp.Regexp + path *regexp.Regexp + linters []string +} + +func (r *severityRule) isEmpty() bool { + return r.text == nil && r.path == nil && len(r.linters) == 0 +} + +type SeverityRule struct { + Severity string + Text string + Source string + Path string + Linters []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{ + 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 + if p.match(i, &rule) { + severity := p.defaultSeverity + if rule.severity != "" { + severity = rule.severity + } + i.Severity = severity + return i + } + } + i.Severity = p.defaultSeverity + return i + }), nil +} + +func (p SeverityRules) matchLinter(i *result.Issue, r *severityRule) bool { + for _, linter := range r.linters { + if linter == i.FromLinter { + return true + } + } + + return false +} + +func (p SeverityRules) matchSource(i *result.Issue, r *severityRule) 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 SeverityRules) match(i *result.Issue, r *severityRule) 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 (SeverityRules) Name() string { return "severity-rules" } +func (SeverityRules) Finish() {} + +var _ Processor = SeverityRules{} From 21bb156773d7f2226d147bf304d255a423ecce19 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Mon, 18 May 2020 17:26:48 -0400 Subject: [PATCH 2/9] fix lint issues --- pkg/config/config.go | 4 ++-- pkg/result/processors/severity_rules.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index b7d692a0b7aa..595dd88ddc1f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -422,7 +422,7 @@ func validateOptionalRegex(value string) error { return err } -func (e ExcludeRule) Validate() error { +func (e ExcludeRule) Validate() error { // nolint:dupl if err := validateOptionalRegex(e.Path); err != nil { return fmt.Errorf("invalid path regex: %v", err) } @@ -460,7 +460,7 @@ type SeverityRule struct { Source string } -func (s *SeverityRule) Validate() error { +func (s *SeverityRule) Validate() error { // nolint:dupl if err := validateOptionalRegex(s.Path); err != nil { return fmt.Errorf("invalid path regex: %v", err) } diff --git a/pkg/result/processors/severity_rules.go b/pkg/result/processors/severity_rules.go index 2ffc44c6a8bb..625b66d8d315 100644 --- a/pkg/result/processors/severity_rules.go +++ b/pkg/result/processors/severity_rules.go @@ -108,7 +108,7 @@ func (p SeverityRules) matchSource(i *result.Issue, r *severityRule) bool { //no return r.source.MatchString(sourceLine) } -func (p SeverityRules) match(i *result.Issue, r *severityRule) bool { +func (p SeverityRules) match(i *result.Issue, r *severityRule) bool { // nolint:dupl if r.isEmpty() { return false } From 4e58d5ac37fffd5ddcae0b26994c52fd55697fff Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Mon, 18 May 2020 17:33:05 -0400 Subject: [PATCH 3/9] change log child name --- .golangci.example.yml | 2 +- pkg/lint/runner.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.golangci.example.yml b/.golangci.example.yml index f0fba94cd626..0422e5cf9c2a 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -391,7 +391,7 @@ issues: # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message severity-default: error - # When a list of severity rules are provided severity information will be added to lint issues. + # When a list of severity rules are provided, severity information will be added to lint issues. # Severity rules has the same filtering capability as exclude rules. # Only affects out formats that support setting severity information. severity-rules: diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 144742cdd792..6cc3a203d6e9 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -123,7 +123,7 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg), processors.NewSourceCode(lineCache, log.Child("source_code")), processors.NewPathShortener(), - processors.NewSeverityRules(icfg.SeverityDefault, severityRules, lineCache, log.Child("exclude_rules")), + processors.NewSeverityRules(icfg.SeverityDefault, severityRules, lineCache, log.Child("severity_rules")), }, Log: log, }, nil From 701bdca2461adf97a060fe4332e0f22034aab124 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Mon, 18 May 2020 17:35:07 -0400 Subject: [PATCH 4/9] code climate omit severity if empty --- pkg/printers/codeclimate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/printers/codeclimate.go b/pkg/printers/codeclimate.go index 980c51de44ab..08c54f8ec5ac 100644 --- a/pkg/printers/codeclimate.go +++ b/pkg/printers/codeclimate.go @@ -14,7 +14,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"` + Severity string `json:"severity,omitempty"` Fingerprint string `json:"fingerprint"` Location struct { Path string `json:"path"` From 194855c1748a0224abe44c722946ad90880465b2 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sun, 24 May 2020 01:13:49 -0400 Subject: [PATCH 5/9] add tests for severity rules, add support for case sensitive rules, fix lint issues, better doc comments, share processor test --- .gitignore | 2 + .golangci.example.yml | 20 ++- pkg/config/config.go | 5 +- pkg/lint/runner.go | 20 ++- pkg/result/processors/exclude_rules_test.go | 43 ++--- pkg/result/processors/exclude_test.go | 28 +--- pkg/result/processors/processor_test.go | 51 ++++++ pkg/result/processors/severity_rules.go | 20 ++- pkg/result/processors/severity_rules_test.go | 155 ++++++++++++++++++ .../processors/testdata/severity_rules.go | 3 + 10 files changed, 281 insertions(+), 66 deletions(-) create mode 100644 pkg/result/processors/processor_test.go create mode 100644 pkg/result/processors/severity_rules_test.go create mode 100644 pkg/result/processors/testdata/severity_rules.go diff --git a/.gitignore b/.gitignore index 896f0c3e5e9a..8e5b12b54241 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ /.vscode/ *.test .DS_Store +coverage.out +coverage.xml \ No newline at end of file diff --git a/.golangci.example.yml b/.golangci.example.yml index 0422e5cf9c2a..e8e8c28060a9 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -383,16 +383,24 @@ issues: # Show only new issues created in git patch with set file path. new-from-patch: path/to/patch/file + # 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. + # 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 + # - 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 severity-default: error - # When a list of severity rules are provided, severity information will be added to lint issues. - # Severity rules has the same filtering capability as exclude rules. + # The default value is false. + # If set to true severity-rules regular expressions become case sensitive. + severity-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. severity-rules: - linters: diff --git a/pkg/config/config.go b/pkg/config/config.go index 595dd88ddc1f..cdf402ed373c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -497,8 +497,9 @@ type Issues struct { ExcludeRules []ExcludeRule `mapstructure:"exclude-rules"` UseDefaultExcludes bool `mapstructure:"exclude-use-default"` - SeverityDefault string `mapstructure:"severity-default"` - SeverityRules []SeverityRule `mapstructure:"severity-rules"` + SeverityDefault string `mapstructure:"severity-default"` + SeverityCaseSensitive bool `mapstructure:"severity-case-sensitive"` + SeverityRules []SeverityRule `mapstructure:"severity-rules"` MaxIssuesPerLinter int `mapstructure:"max-issues-per-linter"` MaxSameIssues int `mapstructure:"max-same-issues"` diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 6cc3a203d6e9..9c21f6f94ee0 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -29,6 +29,7 @@ type Runner struct { Log logutils.Log } +//nolint:funlen 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 @@ -95,6 +96,23 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint }) } + var severityRulesProcessor processors.Processor + if cfg.Issues.SeverityCaseSensitive { + severityRulesProcessor = processors.NewSeverityRulesCaseSensitive( + icfg.SeverityDefault, + severityRules, + lineCache, + log.Child("severity_rules"), + ) + } else { + severityRulesProcessor = processors.NewSeverityRules( + icfg.SeverityDefault, + severityRules, + lineCache, + log.Child("severity_rules"), + ) + } + return &Runner{ Processors: []processors.Processor{ processors.NewCgo(goenv), @@ -123,7 +141,7 @@ func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env, es *lint processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg), processors.NewSourceCode(lineCache, log.Child("source_code")), processors.NewPathShortener(), - processors.NewSeverityRules(icfg.SeverityDefault, severityRules, lineCache, log.Child("severity_rules")), + severityRulesProcessor, }, Log: log, }, nil diff --git a/pkg/result/processors/exclude_rules_test.go b/pkg/result/processors/exclude_rules_test.go index 80759d4f286f..a228586956bb 100644 --- a/pkg/result/processors/exclude_rules_test.go +++ b/pkg/result/processors/exclude_rules_test.go @@ -1,7 +1,6 @@ package processors import ( - "go/token" "path/filepath" "testing" @@ -32,7 +31,7 @@ func TestExcludeRulesMultiple(t *testing.T) { }, }, 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 +42,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,24 +62,6 @@ 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{ { @@ -110,7 +91,7 @@ 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) { @@ -134,7 +115,7 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) { }, }, 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 +128,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"}, @@ -199,5 +180,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")) } diff --git a/pkg/result/processors/exclude_test.go b/pkg/result/processors/exclude_test.go index 7965f2fd26bc..3b92ccd75f83 100644 --- a/pkg/result/processors/exclude_test.go +++ b/pkg/result/processors/exclude_test.go @@ -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...) diff --git a/pkg/result/processors/processor_test.go b/pkg/result/processors/processor_test.go new file mode 100644 index 000000000000..02e7a666aac4 --- /dev/null +++ b/pkg/result/processors/processor_test.go @@ -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) +} diff --git a/pkg/result/processors/severity_rules.go b/pkg/result/processors/severity_rules.go index 625b66d8d315..4705b88b1d12 100644 --- a/pkg/result/processors/severity_rules.go +++ b/pkg/result/processors/severity_rules.go @@ -17,7 +17,7 @@ type severityRule struct { } func (r *severityRule) isEmpty() bool { - return r.text == nil && r.path == nil && len(r.linters) == 0 + return r.source == nil && r.text == nil && r.path == nil && len(r.linters) == 0 } type SeverityRule struct { @@ -134,3 +134,21 @@ 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" } diff --git a/pkg/result/processors/severity_rules_test.go b/pkg/result/processors/severity_rules_test.go new file mode 100644 index 000000000000..f86cc745da40 --- /dev/null +++ b/pkg/result/processors/severity_rules_test.go @@ -0,0 +1,155 @@ +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{ + { + Text: "^ssl$", + Linters: []string{"gosec"}, + Severity: "info", + }, + { + Linters: []string{"linter"}, + Path: "e.go", + Severity: "info", + }, + { + Text: "^testonly$", + Path: `_test\.go`, + Severity: "info", + }, + { + Source: "^//go:generate ", + Linters: []string{"lll"}, + }, + { + Source: "^//go:dosomething", + Severity: "info", + }, + { + Linters: []string{"someotherlinter"}, + Severity: "info", + }, + { + Linters: []string{"somelinter"}, + Severity: "info", + }, + { + 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{ + { + 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{ + { + Text: "^ssl$", + Linters: []string{"gosec", "someotherlinter"}, + Severity: "info", + }, + }, 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) +} diff --git a/pkg/result/processors/testdata/severity_rules.go b/pkg/result/processors/testdata/severity_rules.go new file mode 100644 index 000000000000..026fb2aa13b5 --- /dev/null +++ b/pkg/result/processors/testdata/severity_rules.go @@ -0,0 +1,3 @@ +package testdata + +//go:dosomething From 7606fd9d0d55e78e32a8dec958d1dcae8d8b4897 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sun, 24 May 2020 11:48:26 -0400 Subject: [PATCH 6/9] 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 --- .golangci.example.yml | 7 +- pkg/config/config.go | 111 ++++++------- pkg/config/reader.go | 4 +- pkg/lint/runner.go | 161 +++++++++++-------- pkg/result/processors/base_rule.go | 69 ++++++++ pkg/result/processors/exclude_rules.go | 63 +------- pkg/result/processors/exclude_rules_test.go | 60 ++++--- pkg/result/processors/severity_rules.go | 75 ++------- pkg/result/processors/severity_rules_test.go | 48 ++++-- 9 files changed, 301 insertions(+), 297 deletions(-) create mode 100644 pkg/result/processors/base_rule.go diff --git a/.golangci.example.yml b/.golangci.example.yml index e8e8c28060a9..7d9317194eb4 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -383,6 +383,7 @@ 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 @@ -391,18 +392,18 @@ issues: # - 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 - severity-default: error + default-severity: error # The default value is false. # If set to true severity-rules regular expressions become case sensitive. - severity-case-sensitive: false + 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. - severity-rules: + rules: - linters: - dupl severity: info diff --git a/pkg/config/config.go b/pkg/config/config.go index cdf402ed373c..a102c48fe6d7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -407,13 +407,17 @@ type Linters struct { Presets []string } -type ExcludeRule struct { +type BaseRule struct { Linters []string Path string Text string Source string } +type ExcludeRule struct { + BaseRule `mapstructure:",squash"` +} + func validateOptionalRegex(value string) error { if value == "" { return nil @@ -422,72 +426,17 @@ func validateOptionalRegex(value string) error { return err } -func (e ExcludeRule) Validate() error { // nolint:dupl - 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 +func (e ExcludeRule) Validate() error { + return validateBaseRule(e.BaseRule, 2) } type SeverityRule struct { + BaseRule `mapstructure:",squash"` Severity string - Linters []string - Path string - Text string - Source string } -func (s *SeverityRule) Validate() error { // nolint:dupl - if err := validateOptionalRegex(s.Path); err != nil { - return fmt.Errorf("invalid path regex: %v", err) - } - if err := validateOptionalRegex(s.Text); err != nil { - return fmt.Errorf("invalid text regex: %v", err) - } - if err := validateOptionalRegex(s.Source); err != nil { - return fmt.Errorf("invalid source regex: %v", err) - } - nonBlank := 0 - if len(s.Linters) > 0 { - nonBlank++ - } - if s.Path != "" { - nonBlank++ - } - if s.Text != "" { - nonBlank++ - } - if s.Source != "" { - nonBlank++ - } - const minConditionsCount = 1 - if nonBlank < minConditionsCount { - return errors.New("at least 1 of (text, source, path, linters) should be set") - } - return nil +func (s *SeverityRule) Validate() error { + return validateBaseRule(s.BaseRule, 1) } type Issues struct { @@ -497,10 +446,6 @@ type Issues struct { ExcludeRules []ExcludeRule `mapstructure:"exclude-rules"` UseDefaultExcludes bool `mapstructure:"exclude-use-default"` - SeverityDefault string `mapstructure:"severity-default"` - SeverityCaseSensitive bool `mapstructure:"severity-case-sensitive"` - SeverityRules []SeverityRule `mapstructure:"severity-rules"` - MaxIssuesPerLinter int `mapstructure:"max-issues-per-linter"` MaxSameIssues int `mapstructure:"max-same-issues"` @@ -511,6 +456,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 @@ -526,6 +477,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 } @@ -535,3 +487,32 @@ func NewDefault() *Config { LintersSettings: defaultLintersSettings, } } + +func validateBaseRule(rule BaseRule, minConditionsCount int) error { + if err := validateOptionalRegex(rule.Path); err != nil { + return fmt.Errorf("invalid path regex: %v", err) + } + if err := validateOptionalRegex(rule.Text); err != nil { + return fmt.Errorf("invalid text regex: %v", err) + } + if err := validateOptionalRegex(rule.Source); err != nil { + return fmt.Errorf("invalid source regex: %v", err) + } + nonBlank := 0 + if len(rule.Linters) > 0 { + nonBlank++ + } + if rule.Path != "" { + nonBlank++ + } + if rule.Text != "" { + nonBlank++ + } + if rule.Source != "" { + nonBlank++ + } + if nonBlank < minConditionsCount { + return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount) + } + return nil +} diff --git a/pkg/config/reader.go b/pkg/config/reader.go index 94429403a592..2e8c2c4ba568 100644 --- a/pkg/config/reader.go +++ b/pkg/config/reader.go @@ -113,10 +113,10 @@ func (r *FileReader) validateConfig() error { return fmt.Errorf("error in exclude rule #%d: %v", i, err) } } - if len(c.Issues.SeverityRules) > 0 && c.Issues.SeverityDefault == "" { + if len(c.Severity.Rules) > 0 && c.Severity.Default == "" { return errors.New("can't set severity rule option: no severity default defined") } - for i, rule := range c.Issues.SeverityRules { + for i, rule := range c.Severity.Rules { if err := rule.Validate(); err != nil { return fmt.Errorf("error in severity rule #%d: %v", i, err) } diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 9c21f6f94ee0..f77778b286f9 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -29,27 +29,8 @@ type Runner struct { Log logutils.Log } -//nolint:funlen 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 @@ -64,55 +45,11 @@ 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") } - var severityRules []processors.SeverityRule - for _, r := range icfg.SeverityRules { - severityRules = append(severityRules, processors.SeverityRule{ - Severity: r.Severity, - Text: r.Text, - Source: r.Source, - Path: r.Path, - Linters: r.Linters, - }) - } - - var severityRulesProcessor processors.Processor - if cfg.Issues.SeverityCaseSensitive { - severityRulesProcessor = processors.NewSeverityRulesCaseSensitive( - icfg.SeverityDefault, - severityRules, - lineCache, - log.Child("severity_rules"), - ) - } else { - severityRulesProcessor = processors.NewSeverityRules( - icfg.SeverityDefault, - severityRules, - lineCache, - log.Child("severity_rules"), - ) - } - return &Runner{ Processors: []processors.Processor{ processors.NewCgo(goenv), @@ -130,18 +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(), - severityRulesProcessor, + getSeverityRulesProcessor(&cfg.Severity, log, lineCache), }, Log: log, }, nil @@ -284,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 +} diff --git a/pkg/result/processors/base_rule.go b/pkg/result/processors/base_rule.go new file mode 100644 index 000000000000..b6ce4f2159e9 --- /dev/null +++ b/pkg/result/processors/base_rule.go @@ -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) +} diff --git a/pkg/result/processors/exclude_rules.go b/pkg/result/processors/exclude_rules.go index 0d5a9c5698eb..d4d6569f4ce3 100644 --- a/pkg/result/processors/exclude_rules.go +++ b/pkg/result/processors/exclude_rules.go @@ -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 { //nolint:dupl - 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() {} diff --git a/pkg/result/processors/exclude_rules_test.go b/pkg/result/processors/exclude_rules_test.go index a228586956bb..c247e6ab0f84 100644 --- a/pkg/result/processors/exclude_rules_test.go +++ b/pkg/result/processors/exclude_rules_test.go @@ -14,20 +14,28 @@ 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) @@ -65,9 +73,9 @@ func TestExcludeRulesMultiple(t *testing.T) { func TestExcludeRulesText(t *testing.T) { p := NewExcludeRules([]ExcludeRule{ { - Text: "^exclude$", - Linters: []string{ - "linter", + BaseRule: BaseRule{ + Text: "^exclude$", + Linters: []string{"linter"}, }, }, }, nil, nil) @@ -98,20 +106,28 @@ 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) @@ -154,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) diff --git a/pkg/result/processors/severity_rules.go b/pkg/result/processors/severity_rules.go index 4705b88b1d12..5f11b54101c9 100644 --- a/pkg/result/processors/severity_rules.go +++ b/pkg/result/processors/severity_rules.go @@ -9,23 +9,13 @@ import ( ) type severityRule struct { + baseRule severity string - text *regexp.Regexp - source *regexp.Regexp - path *regexp.Regexp - linters []string -} - -func (r *severityRule) isEmpty() bool { - return r.source == nil && r.text == nil && r.path == nil && len(r.linters) == 0 } type SeverityRule struct { + BaseRule Severity string - Text string - Source string - Path string - Linters []string } type SeverityRules struct { @@ -49,9 +39,8 @@ func NewSeverityRules(defaultSeverity string, rules []SeverityRule, lineCache *f func createSeverityRules(rules []SeverityRule, prefix string) []severityRule { parsedRules := make([]severityRule, 0, len(rules)) for _, rule := range rules { - parsedRule := severityRule{ - linters: rule.Linters, - } + parsedRule := severityRule{} + parsedRule.linters = rule.Linters parsedRule.severity = rule.Severity if rule.Text != "" { parsedRule.text = regexp.MustCompile(prefix + rule.Text) @@ -74,12 +63,14 @@ func (p SeverityRules) Process(issues []result.Issue) ([]result.Issue, error) { return transformIssues(issues, func(i *result.Issue) *result.Issue { for _, rule := range p.rules { rule := rule - if p.match(i, &rule) { - severity := p.defaultSeverity - if rule.severity != "" { - severity = rule.severity - } - i.Severity = severity + + ruleSeverity := p.defaultSeverity + if rule.severity != "" { + ruleSeverity = rule.severity + } + + if rule.match(i, p.lineCache, p.log) { + i.Severity = ruleSeverity return i } } @@ -88,48 +79,6 @@ func (p SeverityRules) Process(issues []result.Issue) ([]result.Issue, error) { }), nil } -func (p SeverityRules) matchLinter(i *result.Issue, r *severityRule) bool { - for _, linter := range r.linters { - if linter == i.FromLinter { - return true - } - } - - return false -} - -func (p SeverityRules) matchSource(i *result.Issue, r *severityRule) 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 SeverityRules) match(i *result.Issue, r *severityRule) bool { // nolint:dupl - 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 (SeverityRules) Name() string { return "severity-rules" } func (SeverityRules) Finish() {} diff --git a/pkg/result/processors/severity_rules_test.go b/pkg/result/processors/severity_rules_test.go index f86cc745da40..d3ff6b2f2c8f 100644 --- a/pkg/result/processors/severity_rules_test.go +++ b/pkg/result/processors/severity_rules_test.go @@ -17,35 +17,49 @@ func TestSeverityRulesMultiple(t *testing.T) { log := report.NewLogWrapper(logutils.NewStderrLog(""), &report.Data{}) p := NewSeverityRules("error", []SeverityRule{ { - Text: "^ssl$", - Linters: []string{"gosec"}, Severity: "info", + BaseRule: BaseRule{ + Text: "^ssl$", + Linters: []string{"gosec"}, + }, }, { - Linters: []string{"linter"}, - Path: "e.go", Severity: "info", + BaseRule: BaseRule{ + Linters: []string{"linter"}, + Path: "e.go", + }, }, { - Text: "^testonly$", - Path: `_test\.go`, Severity: "info", + BaseRule: BaseRule{ + Text: "^testonly$", + Path: `_test\.go`, + }, }, { - Source: "^//go:generate ", - Linters: []string{"lll"}, + BaseRule: BaseRule{ + Source: "^//go:generate ", + Linters: []string{"lll"}, + }, }, { - Source: "^//go:dosomething", Severity: "info", + BaseRule: BaseRule{ + Source: "^//go:dosomething", + }, }, { - Linters: []string{"someotherlinter"}, Severity: "info", + BaseRule: BaseRule{ + Linters: []string{"someotherlinter"}, + }, }, { - Linters: []string{"somelinter"}, Severity: "info", + BaseRule: BaseRule{ + Linters: []string{"somelinter"}, + }, }, { Severity: "info", @@ -93,8 +107,10 @@ func TestSeverityRulesMultiple(t *testing.T) { func TestSeverityRulesText(t *testing.T) { p := NewSeverityRules("", []SeverityRule{ { - Text: "^severity$", - Linters: []string{"linter"}, + BaseRule: BaseRule{ + Text: "^severity$", + Linters: []string{"linter"}, + }, }, }, nil, nil) texts := []string{"seveRity", "1", "", "serverit", "notseverity"} @@ -124,9 +140,11 @@ func TestSeverityRulesCaseSensitive(t *testing.T) { lineCache := fsutils.NewLineCache(fsutils.NewFileCache()) p := NewSeverityRulesCaseSensitive("error", []SeverityRule{ { - Text: "^ssl$", - Linters: []string{"gosec", "someotherlinter"}, Severity: "info", + BaseRule: BaseRule{ + Text: "^ssl$", + Linters: []string{"gosec", "someotherlinter"}, + }, }, }, lineCache, nil) From 025257f75ef0c618503a7de3112e975a1f708199 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sun, 24 May 2020 11:59:11 -0400 Subject: [PATCH 7/9] put validate function under base rule struct --- pkg/config/config.go | 66 +++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index a102c48fe6d7..b29e8f607d73 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -414,6 +414,37 @@ type BaseRule struct { 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"` } @@ -427,16 +458,18 @@ func validateOptionalRegex(value string) error { } func (e ExcludeRule) Validate() error { - return validateBaseRule(e.BaseRule, 2) + return e.BaseRule.Validate(excludeRuleMinConditionsCount) } +const severityRuleMinConditionsCount = 1 + type SeverityRule struct { BaseRule `mapstructure:",squash"` Severity string } func (s *SeverityRule) Validate() error { - return validateBaseRule(s.BaseRule, 1) + return s.BaseRule.Validate(severityRuleMinConditionsCount) } type Issues struct { @@ -487,32 +520,3 @@ func NewDefault() *Config { LintersSettings: defaultLintersSettings, } } - -func validateBaseRule(rule BaseRule, minConditionsCount int) error { - if err := validateOptionalRegex(rule.Path); err != nil { - return fmt.Errorf("invalid path regex: %v", err) - } - if err := validateOptionalRegex(rule.Text); err != nil { - return fmt.Errorf("invalid text regex: %v", err) - } - if err := validateOptionalRegex(rule.Source); err != nil { - return fmt.Errorf("invalid source regex: %v", err) - } - nonBlank := 0 - if len(rule.Linters) > 0 { - nonBlank++ - } - if rule.Path != "" { - nonBlank++ - } - if rule.Text != "" { - nonBlank++ - } - if rule.Source != "" { - nonBlank++ - } - if nonBlank < minConditionsCount { - return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount) - } - return nil -} From 45963becb7612333fa780f12430ba155254ba4c9 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sun, 24 May 2020 12:02:15 -0400 Subject: [PATCH 8/9] better validation error wording --- pkg/config/reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/reader.go b/pkg/config/reader.go index 2e8c2c4ba568..e6b18a7a690c 100644 --- a/pkg/config/reader.go +++ b/pkg/config/reader.go @@ -114,7 +114,7 @@ func (r *FileReader) validateConfig() error { } } if len(c.Severity.Rules) > 0 && c.Severity.Default == "" { - return errors.New("can't set severity rule option: no severity default defined") + 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 { From 836abe03f4593e0ffef61ea974e300772cea4a06 Mon Sep 17 00:00:00 2001 From: Ryan Currah Date: Sun, 24 May 2020 12:33:11 -0400 Subject: [PATCH 9/9] add Fingerprint and Description methods to Issue struct, made codeclimate reporter easier to read, checkstyle output is now pretty printed --- go.mod | 1 + pkg/printers/checkstyle.go | 4 +++- pkg/printers/codeclimate.go | 36 +++++++++++++----------------------- pkg/result/issue.go | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 9c485d2175ea..5ec160cbb3eb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/printers/checkstyle.go b/pkg/printers/checkstyle.go index b3673dc2ab13..c5b948a98d29 100644 --- a/pkg/printers/checkstyle.go +++ b/pkg/printers/checkstyle.go @@ -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" ) @@ -80,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 } diff --git a/pkg/printers/codeclimate.go b/pkg/printers/codeclimate.go index 08c54f8ec5ac..35a22ce99a72 100644 --- a/pkg/printers/codeclimate.go +++ b/pkg/printers/codeclimate.go @@ -2,7 +2,6 @@ package printers import ( "context" - "crypto/md5" //nolint:gosec "encoding/json" "fmt" @@ -32,32 +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 - - if i.Severity != "" { - issue.Severity = i.Severity - } - - // 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] + 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() + + 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 } diff --git a/pkg/result/issue.go b/pkg/result/issue.go index d5a8f24c6dc9..eafdbc4a958a 100644 --- a/pkg/result/issue.go +++ b/pkg/result/issue.go @@ -1,6 +1,8 @@ package result import ( + "crypto/md5" //nolint:gosec + "fmt" "go/token" "golang.org/x/tools/go/packages" @@ -78,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)) +}