Skip to content

Commit 8fde463

Browse files
authored
rules: support inverted path match (#3617)
1 parent 0b8ebea commit 8fde463

12 files changed

+127
-25
lines changed

.golangci.reference.yml

+5
Original file line numberDiff line numberDiff line change
@@ -2323,6 +2323,11 @@ issues:
23232323
- dupl
23242324
- gosec
23252325

2326+
# Run some linter only for test files by excluding its issues for everything else.
2327+
- path-except: _test\.go
2328+
linters:
2329+
- forbidigo
2330+
23262331
# Exclude known linters from partially hard-vendored code,
23272332
# which is impossible to exclude via `nolint` comments.
23282333
# `/` will be replaced by current OS file path separator to properly work on Windows.

docs/src/docs/usage/false-positives.mdx

+12
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ issues:
8181
- goconst
8282
```
8383
84+
The opposite, excluding reports **except** for specific paths, is also possible.
85+
In the following example, only test files get checked:
86+
87+
```yml
88+
issues:
89+
exclude-rules:
90+
- path-except: '(.+)_test\.go'
91+
linters:
92+
- funlen
93+
- goconst
94+
```
95+
8496
In the following example, all the reports related to the files (`skip-files`) are excluded:
8597

8698
```yml

pkg/config/issues.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,25 @@ type ExcludeRule struct {
125125
BaseRule `mapstructure:",squash"`
126126
}
127127

128-
func (e ExcludeRule) Validate() error {
128+
func (e *ExcludeRule) Validate() error {
129129
return e.BaseRule.Validate(excludeRuleMinConditionsCount)
130130
}
131131

132132
type BaseRule struct {
133-
Linters []string
134-
Path string
135-
Text string
136-
Source string
133+
Linters []string
134+
Path string
135+
PathExcept string `mapstructure:"path-except"`
136+
Text string
137+
Source string
137138
}
138139

139-
func (b BaseRule) Validate(minConditionsCount int) error {
140+
func (b *BaseRule) Validate(minConditionsCount int) error {
140141
if err := validateOptionalRegex(b.Path); err != nil {
141142
return fmt.Errorf("invalid path regex: %v", err)
142143
}
144+
if err := validateOptionalRegex(b.PathExcept); err != nil {
145+
return fmt.Errorf("invalid path-except regex: %v", err)
146+
}
143147
if err := validateOptionalRegex(b.Text); err != nil {
144148
return fmt.Errorf("invalid text regex: %v", err)
145149
}
@@ -150,7 +154,10 @@ func (b BaseRule) Validate(minConditionsCount int) error {
150154
if len(b.Linters) > 0 {
151155
nonBlank++
152156
}
153-
if b.Path != "" {
157+
// Filtering by path counts as one condition, regardless how it is done (one or both).
158+
// Otherwise, a rule with Path and PathExcept set would pass validation
159+
// whereas before the introduction of path-except that wouldn't have been precise enough.
160+
if b.Path != "" || b.PathExcept != "" {
154161
nonBlank++
155162
}
156163
if b.Text != "" {
@@ -160,7 +167,7 @@ func (b BaseRule) Validate(minConditionsCount int) error {
160167
nonBlank++
161168
}
162169
if nonBlank < minConditionsCount {
163-
return fmt.Errorf("at least %d of (text, source, path, linters) should be set", minConditionsCount)
170+
return fmt.Errorf("at least %d of (text, source, path[-except], linters) should be set", minConditionsCount)
164171
}
165172
return nil
166173
}

pkg/lint/runner.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,11 @@ func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, files *fsuti
276276
for _, r := range cfg.ExcludeRules {
277277
excludeRules = append(excludeRules, processors.ExcludeRule{
278278
BaseRule: processors.BaseRule{
279-
Text: r.Text,
280-
Source: r.Source,
281-
Path: r.Path,
282-
Linters: r.Linters,
279+
Text: r.Text,
280+
Source: r.Source,
281+
Path: r.Path,
282+
PathExcept: r.PathExcept,
283+
Linters: r.Linters,
283284
},
284285
})
285286
}
@@ -319,10 +320,11 @@ func getSeverityRulesProcessor(cfg *config.Severity, log logutils.Log, files *fs
319320
severityRules = append(severityRules, processors.SeverityRule{
320321
Severity: r.Severity,
321322
BaseRule: processors.BaseRule{
322-
Text: r.Text,
323-
Source: r.Source,
324-
Path: r.Path,
325-
Linters: r.Linters,
323+
Text: r.Text,
324+
Source: r.Source,
325+
Path: r.Path,
326+
PathExcept: r.PathExcept,
327+
Linters: r.Linters,
326328
},
327329
})
328330
}

pkg/result/processors/base_rule.go

+14-9
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ import (
99
)
1010

1111
type BaseRule struct {
12-
Text string
13-
Source string
14-
Path string
15-
Linters []string
12+
Text string
13+
Source string
14+
Path string
15+
PathExcept string
16+
Linters []string
1617
}
1718

1819
type baseRule struct {
19-
text *regexp.Regexp
20-
source *regexp.Regexp
21-
path *regexp.Regexp
22-
linters []string
20+
text *regexp.Regexp
21+
source *regexp.Regexp
22+
path *regexp.Regexp
23+
pathExcept *regexp.Regexp
24+
linters []string
2325
}
2426

2527
func (r *baseRule) isEmpty() bool {
26-
return r.text == nil && r.source == nil && r.path == nil && len(r.linters) == 0
28+
return r.text == nil && r.source == nil && r.path == nil && r.pathExcept == nil && len(r.linters) == 0
2729
}
2830

2931
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
3638
if r.path != nil && !r.path.MatchString(files.WithPathPrefix(issue.FilePath())) {
3739
return false
3840
}
41+
if r.pathExcept != nil && r.pathExcept.MatchString(issue.FilePath()) {
42+
return false
43+
}
3944
if len(r.linters) != 0 && !r.matchLinter(issue) {
4045
return false
4146
}

pkg/result/processors/exclude_rules.go

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func createRules(rules []ExcludeRule, prefix string) []excludeRule {
4747
path := fsutils.NormalizePathInRegex(rule.Path)
4848
parsedRule.path = regexp.MustCompile(path)
4949
}
50+
if rule.PathExcept != "" {
51+
pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept)
52+
parsedRule.pathExcept = regexp.MustCompile(pathExcept)
53+
}
5054
parsedRules = append(parsedRules, parsedRule)
5155
}
5256
return parsedRules

pkg/result/processors/exclude_rules_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ func TestExcludeRulesMultiple(t *testing.T) {
3434
Path: `_test\.go`,
3535
},
3636
},
37+
{
38+
BaseRule: BaseRule{
39+
Text: "^nontestonly$",
40+
PathExcept: `_test\.go`,
41+
},
42+
},
3743
{
3844
BaseRule: BaseRule{
3945
Source: "^//go:generate ",
@@ -42,13 +48,16 @@ func TestExcludeRulesMultiple(t *testing.T) {
4248
},
4349
}, files, nil)
4450

51+
//nolint:dupl
4552
cases := []issueTestCase{
4653
{Path: "e.go", Text: "exclude", Linter: "linter"},
4754
{Path: "e.go", Text: "some", Linter: "linter"},
4855
{Path: "e_test.go", Text: "normal", Linter: "testlinter"},
4956
{Path: "e_Test.go", Text: "normal", Linter: "testlinter"},
5057
{Path: "e_test.go", Text: "another", Linter: "linter"},
5158
{Path: "e_test.go", Text: "testonly", Linter: "linter"},
59+
{Path: "e.go", Text: "nontestonly", Linter: "linter"},
60+
{Path: "e_test.go", Text: "nontestonly", Linter: "linter"},
5261
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"},
5362
}
5463
var issues []result.Issue
@@ -69,6 +78,7 @@ func TestExcludeRulesMultiple(t *testing.T) {
6978
{Path: "e.go", Text: "some", Linter: "linter"},
7079
{Path: "e_Test.go", Text: "normal", Linter: "testlinter"},
7180
{Path: "e_test.go", Text: "another", Linter: "linter"},
81+
{Path: "e_test.go", Text: "nontestonly", Linter: "linter"},
7282
}
7383
assert.Equal(t, expectedCases, resultingCases)
7484
}
@@ -172,6 +182,7 @@ func TestExcludeRulesCaseSensitiveMultiple(t *testing.T) {
172182
},
173183
}, files, nil)
174184

185+
//nolint:dupl
175186
cases := []issueTestCase{
176187
{Path: "e.go", Text: "exclude", Linter: "linter"},
177188
{Path: "e.go", Text: "excLude", Linter: "linter"},

pkg/result/processors/severity_rules.go

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ func createSeverityRules(rules []SeverityRule, prefix string) []severityRule {
5252
path := fsutils.NormalizePathInRegex(rule.Path)
5353
parsedRule.path = regexp.MustCompile(path)
5454
}
55+
if rule.PathExcept != "" {
56+
pathExcept := fsutils.NormalizePathInRegex(rule.PathExcept)
57+
parsedRule.pathExcept = regexp.MustCompile(pathExcept)
58+
}
5559
parsedRules = append(parsedRules, parsedRule)
5660
}
5761
return parsedRules

pkg/result/processors/severity_rules_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ func TestSeverityRulesMultiple(t *testing.T) {
3939
Path: `_test\.go`,
4040
},
4141
},
42+
{
43+
Severity: "info",
44+
BaseRule: BaseRule{
45+
Text: "^nontestonly$",
46+
PathExcept: `_test\.go`,
47+
},
48+
},
4249
{
4350
BaseRule: BaseRule{
4451
Source: "^//go:generate ",
@@ -72,6 +79,8 @@ func TestSeverityRulesMultiple(t *testing.T) {
7279
{Path: "ssl.go", Text: "ssl", Linter: "gosec"},
7380
{Path: "e.go", Text: "some", Linter: "linter"},
7481
{Path: "e_test.go", Text: "testonly", Linter: "testlinter"},
82+
{Path: "e.go", Text: "nontestonly", Linter: "testlinter"},
83+
{Path: "e_test.go", Text: "nontestonly", Linter: "testlinter"},
7584
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"},
7685
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo"},
7786
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter"},
@@ -97,6 +106,8 @@ func TestSeverityRulesMultiple(t *testing.T) {
97106
{Path: "ssl.go", Text: "ssl", Linter: "gosec", Severity: "info"},
98107
{Path: "e.go", Text: "some", Linter: "linter", Severity: "info"},
99108
{Path: "e_test.go", Text: "testonly", Linter: "testlinter", Severity: "info"},
109+
{Path: "e.go", Text: "nontestonly", Linter: "testlinter", Severity: "info"}, // matched
110+
{Path: "e_test.go", Text: "nontestonly", Linter: "testlinter", Severity: "error"}, // not matched
100111
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll", Severity: "error"},
101112
{Path: filepath.Join("testdata", "severity_rules.go"), Line: 3, Linter: "invalidgo", Severity: "info"},
102113
{Path: "someotherlinter.go", Text: "someotherlinter", Linter: "someotherlinter", Severity: "info"},

test/testdata/configs/path-except.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
linters-settings:
2+
forbidigo:
3+
forbid:
4+
- fmt\.Print.*
5+
- time.Sleep(# no sleeping!)?
6+
7+
issues:
8+
exclude-rules:
9+
# Apply forbidigo only to test files, exclude
10+
# it everywhere else.
11+
- path-except: _test\.go
12+
linters:
13+
- forbidigo

test/testdata/path_except.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//golangcitest:args -Eforbidigo
2+
//golangcitest:config_path testdata/configs/path-except.yml
3+
//golangcitest:expected_exitcode 0
4+
package testdata
5+
6+
import (
7+
"fmt"
8+
"time"
9+
)
10+
11+
func Forbidigo() {
12+
fmt.Printf("too noisy!!!")
13+
time.Sleep(time.Nanosecond)
14+
}

test/testdata/path_except_test.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//golangcitest:args -Eforbidigo
2+
//golangcitest:config_path testdata/configs/path-except.yml
3+
package testdata
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestForbidigo(t *testing.T) {
12+
fmt.Printf("too noisy!!!") // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`"
13+
time.Sleep(time.Nanosecond) // want "no sleeping!"
14+
}

0 commit comments

Comments
 (0)