mirror of
https://github.com/mgechev/revive.git
synced 2024-11-21 17:16:40 +02:00
parent
b4fc3db692
commit
310d1d76e4
31
README.md
31
README.md
@ -425,6 +425,37 @@ warningCode = 0
|
||||
[rule.redefines-builtin-id]
|
||||
```
|
||||
|
||||
### Rule-level file excludes
|
||||
|
||||
You also can setup custom excludes for each rule.
|
||||
|
||||
It's alternative for global `-exclude` program arg.
|
||||
|
||||
```toml
|
||||
ignoreGeneratedHeader = false
|
||||
severity = "warning"
|
||||
confidence = 0.8
|
||||
errorCode = 0
|
||||
warningCode = 0
|
||||
|
||||
[rule.blank-imports]
|
||||
Exclude=["**/*.pb.go"]
|
||||
[rule.context-as-argument]
|
||||
Exclude=["src/somepkg/*.go", "TEST"]
|
||||
```
|
||||
|
||||
You can use following exclude patterns
|
||||
|
||||
1. full paths to files `src/pkg/mypkg/some.go`
|
||||
2. globs `src/**/*.pb.go`
|
||||
3. regexes (should have prefix ~) `~\.(pb|auto|generated)\.go$`
|
||||
4. well-known `TEST` (same as `**/*_test.go`)
|
||||
5. special cases:
|
||||
a. `*` and `~` patterns exclude all files (same effect than disabling the rule)
|
||||
b. `""` (empty) pattern excludes nothing
|
||||
|
||||
> NOTE: do not mess with `exclude` that can be used at top level of TOML file, that mean "exclude package patterns", not "exclude file patterns"
|
||||
|
||||
## Available Rules
|
||||
|
||||
List of all available rules. The rules ported from `golint` are left unchanged and indicated in the `golint` column.
|
||||
|
@ -149,6 +149,14 @@ func parseConfig(path string, config *lint.Config) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse the config file: %v", err)
|
||||
}
|
||||
for k, r := range config.Rules {
|
||||
err := r.Initialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error in config of rule [%s] : [%v]", k, err)
|
||||
}
|
||||
config.Rules[k] = r
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,27 @@ func TestGetConfig(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rule-level file filter excludes", func(t *testing.T) {
|
||||
cfg, err := GetConfig("testdata/rule-level-exclude-850.toml")
|
||||
if err != nil {
|
||||
t.Fatal("should be valid config")
|
||||
}
|
||||
r1 := cfg.Rules["r1"]
|
||||
if len(r1.Exclude) > 0 {
|
||||
t.Fatal("r1 should have empty excludes")
|
||||
}
|
||||
r2 := cfg.Rules["r2"]
|
||||
if len(r2.Exclude) != 1 {
|
||||
t.Fatal("r2 should have exclude set")
|
||||
}
|
||||
if !r2.MustExclude("some/file.go") {
|
||||
t.Fatal("r2 should be initialized and exclude some/file.go")
|
||||
}
|
||||
if r2.MustExclude("some/any-other.go") {
|
||||
t.Fatal("r2 should not exclude some/any-other.go")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetLintingRules(t *testing.T) {
|
||||
|
13
config/testdata/rule-level-exclude-850.toml
vendored
Normal file
13
config/testdata/rule-level-exclude-850.toml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
ignoreGeneratedHeader = false
|
||||
severity = "warning"
|
||||
confidence = 0.8
|
||||
errorCode = 0
|
||||
warningCode = 0
|
||||
|
||||
enableAllRules = false
|
||||
|
||||
[rule.r1]
|
||||
# no excludes
|
||||
|
||||
[rule.r2]
|
||||
exclude=["some/file.go"]
|
@ -3,16 +3,44 @@ package lint
|
||||
// Arguments is type used for the arguments of a rule.
|
||||
type Arguments = []interface{}
|
||||
|
||||
type FileFilters = []*FileFilter
|
||||
|
||||
// RuleConfig is type used for the rule configuration.
|
||||
type RuleConfig struct {
|
||||
Arguments Arguments
|
||||
Severity Severity
|
||||
Disabled bool
|
||||
// Exclude - rule-level file excludes, TOML related (strings)
|
||||
Exclude []string
|
||||
// excludeFilters - regex-based file filters, initialized from Exclude
|
||||
excludeFilters []*FileFilter
|
||||
}
|
||||
|
||||
// Initialize - should be called after reading from TOML file
|
||||
func (rc *RuleConfig) Initialize() error {
|
||||
for _, f := range rc.Exclude {
|
||||
ff, err := ParseFileFilter(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rc.excludeFilters = append(rc.excludeFilters, ff)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RulesConfig defines the config for all rules.
|
||||
type RulesConfig = map[string]RuleConfig
|
||||
|
||||
// MustExclude - checks if given filename `name` must be excluded
|
||||
func (rcfg *RuleConfig) MustExclude(name string) bool {
|
||||
for _, exclude := range rcfg.excludeFilters {
|
||||
if exclude.MatchFileName(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DirectiveConfig is type used for the linter directive configuration.
|
||||
type DirectiveConfig struct {
|
||||
Severity Severity
|
||||
|
@ -102,6 +102,9 @@ func (f *File) lint(rules []Rule, config Config, failures chan Failure) {
|
||||
disabledIntervals := f.disabledIntervals(rules, mustSpecifyDisableReason, failures)
|
||||
for _, currentRule := range rules {
|
||||
ruleConfig := rulesConfig[currentRule.Name()]
|
||||
if ruleConfig.MustExclude(f.Name) {
|
||||
continue
|
||||
}
|
||||
currentFailures := currentRule.Apply(f, ruleConfig.Arguments)
|
||||
for idx, failure := range currentFailures {
|
||||
if failure.RuleName == "" {
|
||||
|
128
lint/filefilter.go
Normal file
128
lint/filefilter.go
Normal file
@ -0,0 +1,128 @@
|
||||
package lint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileFilter - file filter to exclude some files for rule
|
||||
// supports whole
|
||||
// 1. file/dir names : pkg/mypkg/my.go,
|
||||
// 2. globs: **/*.pb.go,
|
||||
// 3. regexes (~ prefix) ~-tmp\.\d+\.go
|
||||
// 4. special test marker `TEST` - treats as `~_test\.go`
|
||||
type FileFilter struct {
|
||||
// raw definition of filter inside config
|
||||
raw string
|
||||
// don't care what was at start, will use regexes inside
|
||||
rx *regexp.Regexp
|
||||
// marks filter as matching everything
|
||||
matchesAll bool
|
||||
// marks filter as matching nothing
|
||||
matchesNothing bool
|
||||
}
|
||||
|
||||
// ParseFileFilter - creates [FileFilter] for given raw filter
|
||||
// if empty string, it matches nothing
|
||||
// if `*`, or `~`, it matches everything
|
||||
// while regexp could be invalid, it could return it's compilation error
|
||||
func ParseFileFilter(rawFilter string) (*FileFilter, error) {
|
||||
rawFilter = strings.TrimSpace(rawFilter)
|
||||
result := new(FileFilter)
|
||||
result.raw = rawFilter
|
||||
result.matchesNothing = len(result.raw) == 0
|
||||
result.matchesAll = result.raw == "*" || result.raw == "~"
|
||||
if !result.matchesAll && !result.matchesNothing {
|
||||
if err := result.prepareRegexp(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ff *FileFilter) String() string { return ff.raw }
|
||||
|
||||
// MatchFileName - checks if file name matches filter
|
||||
func (ff *FileFilter) MatchFileName(name string) bool {
|
||||
if ff.matchesAll {
|
||||
return true
|
||||
}
|
||||
if ff.matchesNothing {
|
||||
return false
|
||||
}
|
||||
name = strings.ReplaceAll(name, "\\", "/")
|
||||
return ff.rx.MatchString(name)
|
||||
}
|
||||
|
||||
var fileFilterInvalidGlobRegexp = regexp.MustCompile(`[^/]\*\*[^/]`)
|
||||
var escapeRegexSymbols = ".+{}()[]^$"
|
||||
|
||||
func (ff *FileFilter) prepareRegexp() error {
|
||||
var err error
|
||||
var src = ff.raw
|
||||
if src == "TEST" {
|
||||
src = "~_test\\.go"
|
||||
}
|
||||
if strings.HasPrefix(src, "~") {
|
||||
ff.rx, err = regexp.Compile(src[1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file filter [%s], regexp compile error: [%v]", ff.raw, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
/* globs */
|
||||
if strings.Contains(src, "*") {
|
||||
if fileFilterInvalidGlobRegexp.MatchString(src) {
|
||||
return fmt.Errorf("invalid file filter [%s], invalid glob pattern", ff.raw)
|
||||
}
|
||||
var rxBuild strings.Builder
|
||||
rxBuild.WriteByte('^')
|
||||
wasStar := false
|
||||
justDirGlob := false
|
||||
for _, c := range src {
|
||||
if c == '*' {
|
||||
if wasStar {
|
||||
rxBuild.WriteString(`[\s\S]*`)
|
||||
wasStar = false
|
||||
justDirGlob = true
|
||||
continue
|
||||
}
|
||||
wasStar = true
|
||||
continue
|
||||
}
|
||||
if wasStar {
|
||||
rxBuild.WriteString("[^/]*")
|
||||
wasStar = false
|
||||
}
|
||||
if strings.ContainsRune(escapeRegexSymbols, c) {
|
||||
rxBuild.WriteByte('\\')
|
||||
}
|
||||
rxBuild.WriteRune(c)
|
||||
if c == '/' && justDirGlob {
|
||||
rxBuild.WriteRune('?')
|
||||
}
|
||||
justDirGlob = false
|
||||
}
|
||||
if wasStar {
|
||||
rxBuild.WriteString("[^/]*")
|
||||
}
|
||||
rxBuild.WriteByte('$')
|
||||
ff.rx, err = regexp.Compile(rxBuild.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file filter [%s], regexp compile error after glob expand: [%v]", ff.raw, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// it's whole file mask, just escape dots and normilze separators
|
||||
fillRx := src
|
||||
fillRx = strings.ReplaceAll(fillRx, "\\", "/")
|
||||
fillRx = strings.ReplaceAll(fillRx, ".", `\.`)
|
||||
fillRx = "^" + fillRx + "$"
|
||||
ff.rx, err = regexp.Compile(fillRx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid file filter [%s], regexp compile full path: [%v]", ff.raw, err)
|
||||
}
|
||||
return nil
|
||||
}
|
128
lint/filefilter_test.go
Normal file
128
lint/filefilter_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package lint_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
func TestFileFilter(t *testing.T) {
|
||||
t.Run("whole file name", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("a/b/c.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ff.MatchFileName("a/b/c.go") {
|
||||
t.Fatal("should match a/b/c.go")
|
||||
}
|
||||
if ff.MatchFileName("a/b/d.go") {
|
||||
t.Fatal("should not match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("regex", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("~b/[cd].go$")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ff.MatchFileName("a/b/c.go") {
|
||||
t.Fatal("should match a/b/c.go")
|
||||
}
|
||||
if !ff.MatchFileName("b/d.go") {
|
||||
t.Fatal("should match b/d.go")
|
||||
}
|
||||
if ff.MatchFileName("b/x.go") {
|
||||
t.Fatal("should not match b/x.go")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TEST well-known", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("TEST")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ff.MatchFileName("a/b/c_test.go") {
|
||||
t.Fatal("should match a/b/c_test.go")
|
||||
}
|
||||
if ff.MatchFileName("a/b/c_test_no.go") {
|
||||
t.Fatal("should not match a/b/c_test_no.go")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("glob *", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("a/b/*.pb.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ff.MatchFileName("a/b/xxx.pb.go") {
|
||||
t.Fatal("should match a/b/xxx.pb.go")
|
||||
}
|
||||
if !ff.MatchFileName("a/b/yyy.pb.go") {
|
||||
t.Fatal("should match a/b/yyy.pb.go")
|
||||
}
|
||||
if ff.MatchFileName("a/b/xxx.nopb.go") {
|
||||
t.Fatal("should not match a/b/xxx.nopb.go")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("glob **", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("a/**/*.pb.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ff.MatchFileName("a/x/xxx.pb.go") {
|
||||
t.Fatal("should match a/x/xxx.pb.go")
|
||||
}
|
||||
if !ff.MatchFileName("a/xxx.pb.go") {
|
||||
t.Fatal("should match a/xxx.pb.go")
|
||||
}
|
||||
if !ff.MatchFileName("a/x/y/z/yyy.pb.go") {
|
||||
t.Fatal("should match a/x/y/z/yyy.pb.go")
|
||||
}
|
||||
if ff.MatchFileName("a/b/xxx.nopb.go") {
|
||||
t.Fatal("should not match a/b/xxx.nopb.go")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"}
|
||||
for _, fn := range fileNames {
|
||||
if ff.MatchFileName(fn) {
|
||||
t.Fatalf("should not match %s", fn)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("just *", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"}
|
||||
for _, fn := range fileNames {
|
||||
if !ff.MatchFileName(fn) {
|
||||
t.Fatalf("should match %s", fn)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("just ~", func(t *testing.T) {
|
||||
ff, err := lint.ParseFileFilter("~")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fileNames := []string{"pb.go", "a/pb.go", "a/x/xxx.pb.go", "a/x/xxx.pb_test.go"}
|
||||
for _, fn := range fileNames {
|
||||
if !ff.MatchFileName(fn) {
|
||||
t.Fatalf("should match %s", fn)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
48
test/file-filter_test.go
Normal file
48
test/file-filter_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mgechev/revive/lint"
|
||||
)
|
||||
|
||||
type TestFileFilterRule struct {
|
||||
WasApplyed bool
|
||||
}
|
||||
|
||||
var _ lint.Rule = (*TestFileFilterRule)(nil)
|
||||
|
||||
func (*TestFileFilterRule) Name() string { return "test-file-filter" }
|
||||
func (tfr *TestFileFilterRule) Apply(*lint.File, lint.Arguments) []lint.Failure {
|
||||
tfr.WasApplyed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFileExcludeFilterAtRuleLevel(t *testing.T) {
|
||||
t.Run("is called if no excludes", func(t *testing.T) {
|
||||
rule := &TestFileFilterRule{}
|
||||
testRule(t, "file-to-exclude", rule, &lint.RuleConfig{})
|
||||
if !rule.WasApplyed {
|
||||
t.Fatal("should call rule if no excludes")
|
||||
}
|
||||
})
|
||||
t.Run("is called if exclude not match", func(t *testing.T) {
|
||||
rule := &TestFileFilterRule{}
|
||||
cfg := &lint.RuleConfig{Exclude: []string{"no-matched.go"}}
|
||||
cfg.Initialize()
|
||||
testRule(t, "file-to-exclude", rule, cfg)
|
||||
if !rule.WasApplyed {
|
||||
t.Fatal("should call rule if no excludes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not called if exclude not match", func(t *testing.T) {
|
||||
rule := &TestFileFilterRule{}
|
||||
cfg := &lint.RuleConfig{Exclude: []string{"file-to-exclude.go"}}
|
||||
cfg.Initialize()
|
||||
testRule(t, "file-to-exclude", rule, cfg)
|
||||
if rule.WasApplyed {
|
||||
t.Fatal("should not call rule if excluded")
|
||||
}
|
||||
})
|
||||
}
|
2
testdata/file-to-exclude.go
vendored
Normal file
2
testdata/file-to-exclude.go
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
// just to check FileFilter
|
||||
package fixtures
|
Loading…
Reference in New Issue
Block a user