From 8fde4632faec33af22168a1cad789c676583b87f Mon Sep 17 00:00:00 2001 From: Patrick Ohly <patrick.ohly@intel.com> Date: Wed, 31 May 2023 17:25:59 +0200 Subject: [PATCH] rules: support inverted path match (#3617) --- .golangci.reference.yml | 5 +++++ docs/src/docs/usage/false-positives.mdx | 12 ++++++++++ pkg/config/issues.go | 23 +++++++++++++------- pkg/lint/runner.go | 18 ++++++++------- pkg/result/processors/base_rule.go | 23 ++++++++++++-------- pkg/result/processors/exclude_rules.go | 4 ++++ pkg/result/processors/exclude_rules_test.go | 11 ++++++++++ pkg/result/processors/severity_rules.go | 4 ++++ pkg/result/processors/severity_rules_test.go | 11 ++++++++++ test/testdata/configs/path-except.yml | 13 +++++++++++ test/testdata/path_except.go | 14 ++++++++++++ test/testdata/path_except_test.go | 14 ++++++++++++ 12 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 test/testdata/configs/path-except.yml create mode 100644 test/testdata/path_except.go create mode 100644 test/testdata/path_except_test.go diff --git a/.golangci.reference.yml b/.golangci.reference.yml index 29d4414c..a23840f6 100644 --- a/.golangci.reference.yml +++ b/.golangci.reference.yml @@ -2323,6 +2323,11 @@ issues: - dupl - gosec + # Run some linter only for test files by excluding its issues for everything else. + - path-except: _test\.go + linters: + - forbidigo + # Exclude known linters from partially hard-vendored code, # which is impossible to exclude via `nolint` comments. # `/` will be replaced by current OS file path separator to properly work on Windows. diff --git a/docs/src/docs/usage/false-positives.mdx b/docs/src/docs/usage/false-positives.mdx index fe94197a..20b91a5b 100644 --- a/docs/src/docs/usage/false-positives.mdx +++ b/docs/src/docs/usage/false-positives.mdx @@ -81,6 +81,18 @@ issues: - goconst ``` +The opposite, excluding reports **except** for specific paths, is also possible. +In the following example, only test files get checked: + +```yml +issues: + exclude-rules: + - path-except: '(.+)_test\.go' + linters: + - funlen + - goconst +``` + In the following example, all the reports related to the files (`skip-files`) are excluded: ```yml diff --git a/pkg/config/issues.go b/pkg/config/issues.go index b2437ec9..417b28bd 100644 --- a/pkg/config/issues.go +++ b/pkg/config/issues.go @@ -125,21 +125,25 @@ type ExcludeRule struct { BaseRule `mapstructure:",squash"` } -func (e ExcludeRule) Validate() error { +func (e *ExcludeRule) Validate() error { return e.BaseRule.Validate(excludeRuleMinConditionsCount) } type BaseRule struct { - Linters []string - Path string - Text string - Source string + Linters []string + Path string + PathExcept string `mapstructure:"path-except"` + Text string + Source string } -func (b BaseRule) Validate(minConditionsCount int) error { +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.PathExcept); err != nil { + return fmt.Errorf("invalid path-except regex: %v", err) + } if err := validateOptionalRegex(b.Text); err != nil { return fmt.Errorf("invalid text regex: %v", err) } @@ -150,7 +154,10 @@ func (b BaseRule) Validate(minConditionsCount int) error { if len(b.Linters) > 0 { nonBlank++ } - if b.Path != "" { + // Filtering by path counts as one condition, regardless how it is done (one or both). + // Otherwise, a rule with Path and PathExcept set would pass validation + // whereas before the introduction of path-except that wouldn't have been precise enough. + if b.Path != "" || b.PathExcept != "" { nonBlank++ } if b.Text != "" { @@ -160,7 +167,7 @@ func (b BaseRule) Validate(minConditionsCount int) error { nonBlank++ } if nonBlank < minConditionsCount { - return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount) + return fmt.Errorf("at least %d of (text, source, path[-except], linters) should be set", minConditionsCount) } return nil } diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index b1180438..d270892d 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -276,10 +276,11 @@ func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, files *fsuti 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, + Text: r.Text, + Source: r.Source, + Path: r.Path, + PathExcept: r.PathExcept, + Linters: r.Linters, }, }) } @@ -319,10 +320,11 @@ func getSeverityRulesProcessor(cfg *config.Severity, log logutils.Log, files *fs severityRules = append(severityRules, processors.SeverityRule{ Severity: r.Severity, BaseRule: processors.BaseRule{ - Text: r.Text, - Source: r.Source, - Path: r.Path, - Linters: r.Linters, + Text: r.Text, + Source: r.Source, + Path: r.Path, + PathExcept: r.PathExcept, + Linters: r.Linters, }, }) } diff --git a/pkg/result/processors/base_rule.go b/pkg/result/processors/base_rule.go index eaed829a..b5e13880 100644 --- a/pkg/result/processors/base_rule.go +++ b/pkg/result/processors/base_rule.go @@ -9,21 +9,23 @@ import ( ) type BaseRule struct { - Text string - Source string - Path string - Linters []string + Text string + Source string + Path string + PathExcept string + Linters []string } type baseRule struct { - text *regexp.Regexp - source *regexp.Regexp - path *regexp.Regexp - linters []string + text *regexp.Regexp + source *regexp.Regexp + path *regexp.Regexp + pathExcept *regexp.Regexp + linters []string } func (r *baseRule) isEmpty() bool { - return r.text == nil && r.source == nil && r.path == nil && len(r.linters) == 0 + return r.text == nil && r.source == nil && r.path == nil && r.pathExcept == nil && len(r.linters) == 0 } func (r *baseRule) match(issue *result.Issue, files *fsutils.Files, log logutils.Log) bool { @@ -36,6 +38,9 @@ func (r *baseRule) match(issue *result.Issue, files *fsutils.Files, log logutils if r.path != nil && !r.path.MatchString(files.WithPathPrefix(issue.FilePath())) { return false } + if r.pathExcept != nil && r.pathExcept.MatchString(issue.FilePath()) { + return false + } if len(r.linters) != 0 && !r.matchLinter(issue) { return false } diff --git a/pkg/result/processors/exclude_rules.go b/pkg/result/processors/exclude_rules.go index 42a24f64..2f7e30b4 100644 --- a/pkg/result/processors/exclude_rules.go +++ b/pkg/result/processors/exclude_rules.go @@ -47,6 +47,10 @@ func createRules(rules []ExcludeRule, prefix string) []excludeRule { path := fsutils.NormalizePathInRegex(rule.Path) parsedRule.path = regexp.MustCompile(path) } + if rule.PathExcept != "" { + pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept) + parsedRule.pathExcept = regexp.MustCompile(pathExcept) + } parsedRules = append(parsedRules, parsedRule) } return parsedRules diff --git a/pkg/result/processors/exclude_rules_test.go b/pkg/result/processors/exclude_rules_test.go index d668f093..247cb320 100644 --- a/pkg/result/processors/exclude_rules_test.go +++ b/pkg/result/processors/exclude_rules_test.go @@ -34,6 +34,12 @@ func TestExcludeRulesMultiple(t *testing.T) { Path: `_test\.go`, }, }, + { + BaseRule: BaseRule{ + Text: "^nontestonly$", + PathExcept: `_test\.go`, + }, + }, { BaseRule: BaseRule{ Source: "^//go:generate ", @@ -42,6 +48,7 @@ func TestExcludeRulesMultiple(t *testing.T) { }, }, files, nil) + //nolint:dupl cases := []issueTestCase{ {Path: "e.go", Text: "exclude", Linter: "linter"}, {Path: "e.go", Text: "some", Linter: "linter"}, @@ -49,6 +56,8 @@ func TestExcludeRulesMultiple(t *testing.T) { {Path: "e_Test.go", Text: "normal", Linter: "testlinter"}, {Path: "e_test.go", Text: "another", Linter: "linter"}, {Path: "e_test.go", Text: "testonly", Linter: "linter"}, + {Path: "e.go", Text: "nontestonly", Linter: "linter"}, + {Path: "e_test.go", Text: "nontestonly", Linter: "linter"}, {Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"}, } var issues []result.Issue @@ -69,6 +78,7 @@ func TestExcludeRulesMultiple(t *testing.T) { {Path: "e.go", Text: "some", Linter: "linter"}, {Path: "e_Test.go", Text: "normal", Linter: "testlinter"}, {Path: "e_test.go", Text: "another", Linter: "linter"}, + {Path: "e_test.go", Text: "nontestonly", Linter: "linter"}, } assert.Equal(t, expectedCases, resultingCases) } @@ -172,6 +182,7 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) { }, }, files, nil) + //nolint:dupl cases := []issueTestCase{ {Path: "e.go", Text: "exclude", Linter: "linter"}, {Path: "e.go", Text: "excLude", Linter: "linter"}, diff --git a/pkg/result/processors/severity_rules.go b/pkg/result/processors/severity_rules.go index 8174ae8c..0a4a643b 100644 --- a/pkg/result/processors/severity_rules.go +++ b/pkg/result/processors/severity_rules.go @@ -52,6 +52,10 @@ func createSeverityRules(rules []SeverityRule, prefix string) []severityRule { path := fsutils.NormalizePathInRegex(rule.Path) parsedRule.path = regexp.MustCompile(path) } + if rule.PathExcept != "" { + pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept) + parsedRule.pathExcept = regexp.MustCompile(pathExcept) + } parsedRules = append(parsedRules, parsedRule) } return parsedRules diff --git a/pkg/result/processors/severity_rules_test.go b/pkg/result/processors/severity_rules_test.go index 637febe2..c142524f 100644 --- a/pkg/result/processors/severity_rules_test.go +++ b/pkg/result/processors/severity_rules_test.go @@ -39,6 +39,13 @@ func TestSeverityRulesMultiple(t *testing.T) { Path: `_test\.go`, }, }, + { + Severity: "info", + BaseRule: BaseRule{ + Text: "^nontestonly$", + PathExcept: `_test\.go`, + }, + }, { BaseRule: BaseRule{ Source: "^//go:generate ", @@ -72,6 +79,8 @@ func TestSeverityRulesMultiple(t *testing.T) { {Path: "ssl.go", Text: "ssl", Linter: "gosec"}, {Path: "e.go", Text: "some", Linter: "linter"}, {Path: "e_test.go", Text: "testonly", Linter: "testlinter"}, + {Path: "e.go", Text: "nontestonly", Linter: "testlinter"}, + {Path: "e_test.go", Text: "nontestonly", 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"}, @@ -97,6 +106,8 @@ func TestSeverityRulesMultiple(t *testing.T) { {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: "e.go", Text: "nontestonly", Linter: "testlinter", Severity: "info"}, // matched + {Path: "e_test.go", Text: "nontestonly", Linter: "testlinter", Severity: "error"}, // not matched {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"}, diff --git a/test/testdata/configs/path-except.yml b/test/testdata/configs/path-except.yml new file mode 100644 index 00000000..712bbd5a --- /dev/null +++ b/test/testdata/configs/path-except.yml @@ -0,0 +1,13 @@ +linters-settings: + forbidigo: + forbid: + - fmt\.Print.* + - time.Sleep(# no sleeping!)? + +issues: + exclude-rules: + # Apply forbidigo only to test files, exclude + # it everywhere else. + - path-except: _test\.go + linters: + - forbidigo diff --git a/test/testdata/path_except.go b/test/testdata/path_except.go new file mode 100644 index 00000000..ab7b33a0 --- /dev/null +++ b/test/testdata/path_except.go @@ -0,0 +1,14 @@ +//golangcitest:args -Eforbidigo +//golangcitest:config_path testdata/configs/path-except.yml +//golangcitest:expected_exitcode 0 +package testdata + +import ( + "fmt" + "time" +) + +func Forbidigo() { + fmt.Printf("too noisy!!!") + time.Sleep(time.Nanosecond) +} diff --git a/test/testdata/path_except_test.go b/test/testdata/path_except_test.go new file mode 100644 index 00000000..5f69f87a --- /dev/null +++ b/test/testdata/path_except_test.go @@ -0,0 +1,14 @@ +//golangcitest:args -Eforbidigo +//golangcitest:config_path testdata/configs/path-except.yml +package testdata + +import ( + "fmt" + "testing" + "time" +) + +func TestForbidigo(t *testing.T) { + fmt.Printf("too noisy!!!") // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`" + time.Sleep(time.Nanosecond) // want "no sleeping!" +}