rules: support inverted path match ()

This commit is contained in:
Patrick Ohly 2023-05-31 17:25:59 +02:00 committed by GitHub
parent 0b8ebea959
commit 8fde4632fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 127 additions and 25 deletions

@ -2323,6 +2323,11 @@ issues:
- dupl - dupl
- gosec - 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, # Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments. # which is impossible to exclude via `nolint` comments.
# `/` will be replaced by current OS file path separator to properly work on Windows. # `/` will be replaced by current OS file path separator to properly work on Windows.

@ -81,6 +81,18 @@ issues:
- goconst - 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: In the following example, all the reports related to the files (`skip-files`) are excluded:
```yml ```yml

@ -125,21 +125,25 @@ type ExcludeRule struct {
BaseRule `mapstructure:",squash"` BaseRule `mapstructure:",squash"`
} }
func (e ExcludeRule) Validate() error { func (e *ExcludeRule) Validate() error {
return e.BaseRule.Validate(excludeRuleMinConditionsCount) return e.BaseRule.Validate(excludeRuleMinConditionsCount)
} }
type BaseRule struct { type BaseRule struct {
Linters []string Linters []string
Path string Path string
Text string PathExcept string `mapstructure:"path-except"`
Source string 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 { if err := validateOptionalRegex(b.Path); err != nil {
return fmt.Errorf("invalid path regex: %v", err) 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 { if err := validateOptionalRegex(b.Text); err != nil {
return fmt.Errorf("invalid text regex: %v", err) return fmt.Errorf("invalid text regex: %v", err)
} }
@ -150,7 +154,10 @@ func (b BaseRule) Validate(minConditionsCount int) error {
if len(b.Linters) > 0 { if len(b.Linters) > 0 {
nonBlank++ 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++ nonBlank++
} }
if b.Text != "" { if b.Text != "" {
@ -160,7 +167,7 @@ func (b BaseRule) Validate(minConditionsCount int) error {
nonBlank++ nonBlank++
} }
if nonBlank < minConditionsCount { 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 return nil
} }

@ -276,10 +276,11 @@ func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, files *fsuti
for _, r := range cfg.ExcludeRules { for _, r := range cfg.ExcludeRules {
excludeRules = append(excludeRules, processors.ExcludeRule{ excludeRules = append(excludeRules, processors.ExcludeRule{
BaseRule: processors.BaseRule{ BaseRule: processors.BaseRule{
Text: r.Text, Text: r.Text,
Source: r.Source, Source: r.Source,
Path: r.Path, Path: r.Path,
Linters: r.Linters, 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{ severityRules = append(severityRules, processors.SeverityRule{
Severity: r.Severity, Severity: r.Severity,
BaseRule: processors.BaseRule{ BaseRule: processors.BaseRule{
Text: r.Text, Text: r.Text,
Source: r.Source, Source: r.Source,
Path: r.Path, Path: r.Path,
Linters: r.Linters, PathExcept: r.PathExcept,
Linters: r.Linters,
}, },
}) })
} }

@ -9,21 +9,23 @@ import (
) )
type BaseRule struct { type BaseRule struct {
Text string Text string
Source string Source string
Path string Path string
Linters []string PathExcept string
Linters []string
} }
type baseRule struct { type baseRule struct {
text *regexp.Regexp text *regexp.Regexp
source *regexp.Regexp source *regexp.Regexp
path *regexp.Regexp path *regexp.Regexp
linters []string pathExcept *regexp.Regexp
linters []string
} }
func (r *baseRule) isEmpty() bool { 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 { 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())) { if r.path != nil && !r.path.MatchString(files.WithPathPrefix(issue.FilePath())) {
return false return false
} }
if r.pathExcept != nil && r.pathExcept.MatchString(issue.FilePath()) {
return false
}
if len(r.linters) != 0 && !r.matchLinter(issue) { if len(r.linters) != 0 && !r.matchLinter(issue) {
return false return false
} }

@ -47,6 +47,10 @@ func createRules(rules []ExcludeRule, prefix string) []excludeRule {
path := fsutils.NormalizePathInRegex(rule.Path) path := fsutils.NormalizePathInRegex(rule.Path)
parsedRule.path = regexp.MustCompile(path) parsedRule.path = regexp.MustCompile(path)
} }
if rule.PathExcept != "" {
pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept)
parsedRule.pathExcept = regexp.MustCompile(pathExcept)
}
parsedRules = append(parsedRules, parsedRule) parsedRules = append(parsedRules, parsedRule)
} }
return parsedRules return parsedRules

@ -34,6 +34,12 @@ func TestExcludeRulesMultiple(t *testing.T) {
Path: `_test\.go`, Path: `_test\.go`,
}, },
}, },
{
BaseRule: BaseRule{
Text: "^nontestonly$",
PathExcept: `_test\.go`,
},
},
{ {
BaseRule: BaseRule{ BaseRule: BaseRule{
Source: "^//go:generate ", Source: "^//go:generate ",
@ -42,6 +48,7 @@ func TestExcludeRulesMultiple(t *testing.T) {
}, },
}, files, nil) }, files, nil)
//nolint:dupl
cases := []issueTestCase{ cases := []issueTestCase{
{Path: "e.go", Text: "exclude", Linter: "linter"}, {Path: "e.go", Text: "exclude", Linter: "linter"},
{Path: "e.go", Text: "some", 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: "normal", Linter: "testlinter"},
{Path: "e_test.go", Text: "another", Linter: "linter"}, {Path: "e_test.go", Text: "another", Linter: "linter"},
{Path: "e_test.go", Text: "testonly", 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"}, {Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"},
} }
var issues []result.Issue var issues []result.Issue
@ -69,6 +78,7 @@ func TestExcludeRulesMultiple(t *testing.T) {
{Path: "e.go", Text: "some", Linter: "linter"}, {Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_Test.go", Text: "normal", Linter: "testlinter"}, {Path: "e_Test.go", Text: "normal", Linter: "testlinter"},
{Path: "e_test.go", Text: "another", Linter: "linter"}, {Path: "e_test.go", Text: "another", Linter: "linter"},
{Path: "e_test.go", Text: "nontestonly", Linter: "linter"},
} }
assert.Equal(t, expectedCases, resultingCases) assert.Equal(t, expectedCases, resultingCases)
} }
@ -172,6 +182,7 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) {
}, },
}, files, nil) }, files, nil)
//nolint:dupl
cases := []issueTestCase{ cases := []issueTestCase{
{Path: "e.go", Text: "exclude", Linter: "linter"}, {Path: "e.go", Text: "exclude", Linter: "linter"},
{Path: "e.go", Text: "excLude", Linter: "linter"}, {Path: "e.go", Text: "excLude", Linter: "linter"},

@ -52,6 +52,10 @@ func createSeverityRules(rules []SeverityRule, prefix string) []severityRule {
path := fsutils.NormalizePathInRegex(rule.Path) path := fsutils.NormalizePathInRegex(rule.Path)
parsedRule.path = regexp.MustCompile(path) parsedRule.path = regexp.MustCompile(path)
} }
if rule.PathExcept != "" {
pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept)
parsedRule.pathExcept = regexp.MustCompile(pathExcept)
}
parsedRules = append(parsedRules, parsedRule) parsedRules = append(parsedRules, parsedRule)
} }
return parsedRules return parsedRules

@ -39,6 +39,13 @@ func TestSeverityRulesMultiple(t *testing.T) {
Path: `_test\.go`, Path: `_test\.go`,
}, },
}, },
{
Severity: "info",
BaseRule: BaseRule{
Text: "^nontestonly$",
PathExcept: `_test\.go`,
},
},
{ {
BaseRule: BaseRule{ BaseRule: BaseRule{
Source: "^//go:generate ", Source: "^//go:generate ",
@ -72,6 +79,8 @@ func TestSeverityRulesMultiple(t *testing.T) {
{Path: "ssl.go", Text: "ssl", Linter: "gosec"}, {Path: "ssl.go", Text: "ssl", Linter: "gosec"},
{Path: "e.go", Text: "some", Linter: "linter"}, {Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_test.go", Text: "testonly", Linter: "testlinter"}, {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", "exclude_rules.go"), Line: 3, Linter: "lll"},
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo"}, {Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo"},
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter"}, {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: "ssl.go", Text: "ssl", Linter: "gosec", Severity: "info"},
{Path: "e.go", Text: "some", Linter: "linter", Severity: "info"}, {Path: "e.go", Text: "some", Linter: "linter", Severity: "info"},
{Path: "e_test.go", Text: "testonly", Linter: "testlinter", 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", "exclude_rules.go"), Line: 3, Linter: "lll", Severity: "error"},
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo", Severity: "info"}, {Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo", Severity: "info"},
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter", Severity: "info"}, {Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter", Severity: "info"},

13
test/testdata/configs/path-except.yml vendored Normal file

@ -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

14
test/testdata/path_except.go vendored Normal file

@ -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)
}

14
test/testdata/path_except_test.go vendored Normal file

@ -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!"
}