Repository: mgechev/revive (go)

Issue:
Glob/regex file excludes at rule level
Where are different policies for some files.
For example we never treat `add-constant` as warning in `*_test.go` files.
For now - we cannot express excludes at rule level - just at global level.

Suggested to add `exclude : []string` for configuration and `exclude: []*regexp.Regex` inside.

To distinguish between regexes and strings we could use `~` prefix for regexes:

```
# revive toml
# bettter to treat Exclude as common thing for all rules
[rule.add-constant]
    Exclude = [ "pkg/x/some.go" , "~_test.go$" ] # first is direct file name, second is regex
    Arguments = [
          {
             allowInts="-1,0,1,2",
             allowStrs="\"\"",
             allowFloats="0.0,1.0",
             ignoreFuncs="os\\.*,fmt\\.*"
          }
   ]
```


Pull request:
per-rule file exclude filters (#850)
Rule-level file exclude filter feature implementation #850

Tryed to keep project at max compatibility level.

But where are some changes in core structures.

Filters are prechecked and initialized after reading from TOML, so it could cause error.
So `Initialize() error` method added to RuleConfig

Logically it should be applyed in `normalize...`  part of GetConfig, but just parse can return error, not normalize, so
added call of initialization to `parse` part of GetConfig

All tested.

Closes #850


Patch:
diff --git a/README.md b/README.md
index 43800ae..2bdb47e 100644
--- a/README.md
+++ b/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.
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 1709188..3d78110 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -735,13 +735,36 @@ _Configuration_: N/A
 
 _Description_: This rule warns on unused parameters. Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug.
 
-_Configuration_: N/A
+_Configuration_: Supports arguments with single of `map[string]any` with option `allowRegex` to provide additional to `_` mask to allowed unused parameter names, for example:
+
+```toml
+[rule.unused-parameter]
+    Arguments = [ { allowRegex = "^_" } ]
+```
+
+allows any names started with `_`, not just `_` itself:
+
+```go
+func SomeFunc(_someObj *MyStruct) {} // matches rule
+```
 
 ## unused-receiver
 
 _Description_: This rule warns on unused method receivers. Methods with unused receivers can be a symptom of an unfinished refactoring or a bug.
 
-_Configuration_: N/A
+_Configuration_: Supports arguments with single of `map[string]any` with option `allowRegex` to provide additional to `_` mask to allowed unused receiver names, for example:
+
+```toml
+[rule.unused-receiver]
+    Arguments = [ { allowRegex = "^_" } ]
+```
+
+allows any names started with `_`, not just `_` itself:
+
+```go
+func (_my *MyStruct) SomeMethod() {} // matches rule
+```
+
 
 ## use-any
 
diff --git a/config/config.go b/config/config.go
index 225a570..7643d27 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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
 }
 
diff --git a/go.mod b/go.mod
index 5228b32..a7e990c 100644
--- a/go.mod
+++ b/go.mod
@@ -11,12 +11,12 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/pkg/errors v0.9.1
-	golang.org/x/tools v0.11.1
+	golang.org/x/tools v0.12.0
 )
 
 require (
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.17 // indirect
 	github.com/mattn/go-runewidth v0.0.9 // indirect
-	golang.org/x/sys v0.10.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 946dadb..f8603b6 100644
--- a/go.sum
+++ b/go.sum
@@ -83,6 +83,8 @@ golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
@@ -107,6 +109,8 @@ golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
 golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
 golang.org/x/tools v0.11.1 h1:ojD5zOW8+7dOGzdnNgersm8aPfcDjhMp12UfG93NIMc=
 golang.org/x/tools v0.11.1/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/lint/config.go b/lint/config.go
index 2763058..9b26d58 100644
--- a/lint/config.go
+++ b/lint/config.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
diff --git a/lint/file.go b/lint/file.go
index dcf0e60..2325530 100644
--- a/lint/file.go
+++ b/lint/file.go
@@ -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 == "" {
diff --git a/lint/filefilter.go b/lint/filefilter.go
new file mode 100644
index 0000000..8da090b
--- /dev/null
+++ b/lint/filefilter.go
@@ -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
+}
diff --git a/rule/imports-blacklist.go b/rule/imports-blacklist.go
index 7106628..bb8262c 100644
--- a/rule/imports-blacklist.go
+++ b/rule/imports-blacklist.go
@@ -52,10 +52,6 @@ func (r *ImportsBlacklistRule) Apply(file *lint.File, arguments lint.Arguments)
 
 	var failures []lint.Failure
 
-	if file.IsTest() {
-		return failures // skip, test file
-	}
-
 	for _, is := range file.AST.Imports {
 		path := is.Path
 		if path != nil && r.isBlacklisted(path.Value) {
diff --git a/rule/unused-param.go b/rule/unused-param.go
index ab3da45..df6cd9a 100644
--- a/rule/unused-param.go
+++ b/rule/unused-param.go
@@ -3,22 +3,72 @@ package rule
 import (
 	"fmt"
 	"go/ast"
+	"regexp"
+	"sync"
 
 	"github.com/mgechev/revive/lint"
 )
 
 // UnusedParamRule lints unused params in functions.
-type UnusedParamRule struct{}
+type UnusedParamRule struct {
+	configured bool
+	// regex to check if some name is valid for unused parameter, "^_$" by default
+	allowRegex *regexp.Regexp
+	failureMsg string
+	sync.Mutex
+}
+
+func (r *UnusedParamRule) configure(args lint.Arguments) {
+	r.Lock()
+	defer r.Unlock()
+
+	if r.configured {
+		return
+	}
+	r.configured = true
+
+	// while by default args is an array, i think it's good to provide structures inside it by default, not arrays or primitives
+	// it's more compatible to JSON nature of configurations
+	var allowedRegexStr string
+	if len(args) == 0 {
+		allowedRegexStr = "^_$"
+		r.failureMsg = "parameter '%s' seems to be unused, consider removing or renaming it as _"
+	} else {
+		// Arguments = [{}]
+		options := args[0].(map[string]interface{})
+		// Arguments = [{allowedRegex="^_"}]
+
+		if allowedRegexParam, ok := options["allowRegex"]; ok {
+			allowedRegexStr, ok = allowedRegexParam.(string)
+			if !ok {
+				panic(fmt.Errorf("error configuring %s rule: allowedRegex is not string but [%T]", r.Name(), allowedRegexParam))
+			}
+		}
+	}
+	var err error
+	r.allowRegex, err = regexp.Compile(allowedRegexStr)
+	if err != nil {
+		panic(fmt.Errorf("error configuring %s rule: allowedRegex is not valid regex [%s]: %v", r.Name(), allowedRegexStr, err))
+	}
+
+	if r.failureMsg == "" {
+		r.failureMsg = "parameter '%s' seems to be unused, consider removing or renaming it to match " + r.allowRegex.String()
+	}
+}
 
 // Apply applies the rule to given file.
-func (*UnusedParamRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+func (r *UnusedParamRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
+	r.configure(args)
 	var failures []lint.Failure
 
 	onFailure := func(failure lint.Failure) {
 		failures = append(failures, failure)
 	}
-
-	w := lintUnusedParamRule{onFailure: onFailure}
+	w := lintUnusedParamRule{
+		onFailure:  onFailure,
+		allowRegex: r.allowRegex,
+		failureMsg: r.failureMsg,
+	}
 
 	ast.Walk(w, file.AST)
 
@@ -31,7 +81,9 @@ func (*UnusedParamRule) Name() string {
 }
 
 type lintUnusedParamRule struct {
-	onFailure func(lint.Failure)
+	onFailure  func(lint.Failure)
+	allowRegex *regexp.Regexp
+	failureMsg string
 }
 
 func (w lintUnusedParamRule) Visit(node ast.Node) ast.Visitor {
@@ -65,12 +117,15 @@ func (w lintUnusedParamRule) Visit(node ast.Node) ast.Visitor {
 
 		for _, p := range n.Type.Params.List {
 			for _, n := range p.Names {
+				if w.allowRegex.FindStringIndex(n.Name) != nil {
+					continue
+				}
 				if params[n.Obj] {
 					w.onFailure(lint.Failure{
 						Confidence: 1,
 						Node:       n,
 						Category:   "bad practice",
-						Failure:    fmt.Sprintf("parameter '%s' seems to be unused, consider removing or renaming it as _", n.Name),
+						Failure:    fmt.Sprintf(w.failureMsg, n.Name),
 					})
 				}
 			}
diff --git a/rule/unused-receiver.go b/rule/unused-receiver.go
index 2289a51..488572b 100644
--- a/rule/unused-receiver.go
+++ b/rule/unused-receiver.go
@@ -3,22 +3,72 @@ package rule
 import (
 	"fmt"
 	"go/ast"
+	"regexp"
+	"sync"
 
 	"github.com/mgechev/revive/lint"
 )
 
 // UnusedReceiverRule lints unused params in functions.
-type UnusedReceiverRule struct{}
+type UnusedReceiverRule struct {
+	configured bool
+	// regex to check if some name is valid for unused parameter, "^_$" by default
+	allowRegex *regexp.Regexp
+	failureMsg string
+	sync.Mutex
+}
+
+func (r *UnusedReceiverRule) configure(args lint.Arguments) {
+	r.Lock()
+	defer r.Unlock()
+
+	if r.configured {
+		return
+	}
+	r.configured = true
+
+	// while by default args is an array, i think it's good to provide structures inside it by default, not arrays or primitives
+	// it's more compatible to JSON nature of configurations
+	var allowedRegexStr string
+	if len(args) == 0 {
+		allowedRegexStr = "^_$"
+		r.failureMsg = "method receiver '%s' is not referenced in method's body, consider removing or renaming it as _"
+	} else {
+		// Arguments = [{}]
+		options := args[0].(map[string]interface{})
+		// Arguments = [{allowedRegex="^_"}]
+
+		if allowedRegexParam, ok := options["allowRegex"]; ok {
+			allowedRegexStr, ok = allowedRegexParam.(string)
+			if !ok {
+				panic(fmt.Errorf("error configuring [unused-receiver] rule: allowedRegex is not string but [%T]", allowedRegexParam))
+			}
+		}
+	}
+	var err error
+	r.allowRegex, err = regexp.Compile(allowedRegexStr)
+	if err != nil {
+		panic(fmt.Errorf("error configuring [unused-receiver] rule: allowedRegex is not valid regex [%s]: %v", allowedRegexStr, err))
+	}
+	if r.failureMsg == "" {
+		r.failureMsg = "method receiver '%s' is not referenced in method's body, consider removing or renaming it to match " + r.allowRegex.String()
+	}
+}
 
 // Apply applies the rule to given file.
-func (*UnusedReceiverRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+func (r *UnusedReceiverRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
+	r.configure(args)
 	var failures []lint.Failure
 
 	onFailure := func(failure lint.Failure) {
 		failures = append(failures, failure)
 	}
 
-	w := lintUnusedReceiverRule{onFailure: onFailure}
+	w := lintUnusedReceiverRule{
+		onFailure:  onFailure,
+		allowRegex: r.allowRegex,
+		failureMsg: r.failureMsg,
+	}
 
 	ast.Walk(w, file.AST)
 
@@ -31,7 +81,9 @@ func (*UnusedReceiverRule) Name() string {
 }
 
 type lintUnusedReceiverRule struct {
-	onFailure func(lint.Failure)
+	onFailure  func(lint.Failure)
+	allowRegex *regexp.Regexp
+	failureMsg string
 }
 
 func (w lintUnusedReceiverRule) Visit(node ast.Node) ast.Visitor {
@@ -51,6 +103,10 @@ func (w lintUnusedReceiverRule) Visit(node ast.Node) ast.Visitor {
 			return nil // the receiver is already named _
 		}
 
+		if w.allowRegex != nil && w.allowRegex.FindStringIndex(recID.Name) != nil {
+			return nil
+		}
+
 		// inspect the func body looking for references to the receiver id
 		fselect := func(n ast.Node) bool {
 			ident, isAnID := n.(*ast.Ident)
@@ -67,7 +123,7 @@ func (w lintUnusedReceiverRule) Visit(node ast.Node) ast.Visitor {
 			Confidence: 1,
 			Node:       recID,
 			Category:   "bad practice",
-			Failure:    fmt.Sprintf("method receiver '%s' is not referenced in method's body, consider removing or renaming it as _", recID.Name),
+			Failure:    fmt.Sprintf(w.failureMsg, recID.Name),
 		})
 
 		return nil // full method body already inspected


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 57b7a97..42dff4b 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -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) {
diff --git a/config/testdata/rule-level-exclude-850.toml b/config/testdata/rule-level-exclude-850.toml
new file mode 100644
index 0000000..97523e8
--- /dev/null
+++ b/config/testdata/rule-level-exclude-850.toml
@@ -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"]
\ No newline at end of file
diff --git a/lint/filefilter_test.go b/lint/filefilter_test.go
new file mode 100644
index 0000000..6c20afd
--- /dev/null
+++ b/lint/filefilter_test.go
@@ -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)
+			}
+		}
+
+	})
+}
diff --git a/test/file-filter_test.go b/test/file-filter_test.go
new file mode 100644
index 0000000..7fce3b0
--- /dev/null
+++ b/test/file-filter_test.go
@@ -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")
+		}
+	})
+}
diff --git a/test/unused-param_test.go b/test/unused-param_test.go
index 7b6472c..8be1494 100644
--- a/test/unused-param_test.go
+++ b/test/unused-param_test.go
@@ -3,11 +3,15 @@ package test
 import (
 	"testing"
 
+	"github.com/mgechev/revive/lint"
 	"github.com/mgechev/revive/rule"
 )
 
 func TestUnusedParam(t *testing.T) {
 	testRule(t, "unused-param", &rule.UnusedParamRule{})
+	testRule(t, "unused-param-custom-regex", &rule.UnusedParamRule{}, &lint.RuleConfig{Arguments: []interface{}{
+		map[string]interface{}{"allowRegex": "^xxx"},
+	}})
 }
 
 func BenchmarkUnusedParam(b *testing.B) {
diff --git a/test/unused-receiver_test.go b/test/unused-receiver_test.go
index e4bae7c..2fb6a0e 100644
--- a/test/unused-receiver_test.go
+++ b/test/unused-receiver_test.go
@@ -3,9 +3,13 @@ package test
 import (
 	"testing"
 
+	"github.com/mgechev/revive/lint"
 	"github.com/mgechev/revive/rule"
 )
 
 func TestUnusedReceiver(t *testing.T) {
 	testRule(t, "unused-receiver", &rule.UnusedReceiverRule{})
+	testRule(t, "unused-receiver-custom-regex", &rule.UnusedReceiverRule{}, &lint.RuleConfig{Arguments: []interface{}{
+		map[string]interface{}{"allowRegex": "^xxx"},
+	}})
 }
diff --git a/testdata/file-to-exclude.go b/testdata/file-to-exclude.go
new file mode 100644
index 0000000..58283be
--- /dev/null
+++ b/testdata/file-to-exclude.go
@@ -0,0 +1,2 @@
+// just to check FileFilter
+package fixtures
diff --git a/testdata/unused-param-custom-regex.go b/testdata/unused-param-custom-regex.go
new file mode 100644
index 0000000..24612ae
--- /dev/null
+++ b/testdata/unused-param-custom-regex.go
@@ -0,0 +1,12 @@
+package fixtures
+
+// all will ok with xxxParam if Arguments = [{allowRegex="^xxx"}]
+
+func f0(xxxParam int) {}
+
+// still works with _
+
+func f1(_ int) {}
+
+func f2(yyyParam int) { // MATCH /parameter 'yyyParam' seems to be unused, consider removing or renaming it to match ^xxx/
+}
diff --git a/testdata/unused-receiver-custom-regex.go b/testdata/unused-receiver-custom-regex.go
new file mode 100644
index 0000000..3fdcd4a
--- /dev/null
+++ b/testdata/unused-receiver-custom-regex.go
@@ -0,0 +1,12 @@
+package fixtures
+
+// all will ok with xxxParam if Arguments = [{allowRegex="^xxx"}]
+
+func (xxxParam *SomeObj) f0() {}
+
+// still works with _
+
+func (_ *SomeObj) f1() {}
+
+func (yyyParam *SomeObj) f2() { // MATCH /method receiver 'yyyParam' is not referenced in method's body, consider removing or renaming it to match ^xxx/
+}