Repository: mgechev/revive (go)

Issue:
support private `_UPPER_CASED_CONST` with `var-naming` and `upperCaseConst=true`  
I see new feature added last week #851 with allowing to use uppercase constants.

I think there should be also a **private** uppercase constant support for this feature.

```
const _SOME_PRIVATE_CONST = "hello, world"
```

Pull request:
(var-naming) support private uppercase constants #865
`var-naming` rule with `upperCaseConst=true` now allows to use private uppercase constants

Closes #865

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/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
diff --git a/rule/var-naming.go b/rule/var-naming.go
index 2aa5ae2..c6a6305 100644
--- a/rule/var-naming.go
+++ b/rule/var-naming.go
@@ -13,8 +13,8 @@ import (
 
 var anyCapsRE = regexp.MustCompile(`[A-Z]`)
 
-// regexp for constant names like `SOME_CONST`, `SOME_CONST_2`, `X123_3` (#851)
-var upperCaseConstRE = regexp.MustCompile(`^[A-Z][A-Z\d]*(_[A-Z\d]+)*$`)
+// regexp for constant names like `SOME_CONST`, `SOME_CONST_2`, `X123_3`, `_SOME_PRIVATE_CONST` (#851, #865)
+var upperCaseConstRE = regexp.MustCompile(`^_?[A-Z][A-Z\d]*(_[A-Z\d]+)*$`)
 
 // VarNamingRule lints given else constructs.
 type VarNamingRule struct {
@@ -27,34 +27,36 @@ type VarNamingRule struct {
 
 func (r *VarNamingRule) configure(arguments lint.Arguments) {
 	r.Lock()
-	if !r.configured {
-		if len(arguments) >= 1 {
-			r.whitelist = getList(arguments[0], "whitelist")
-		}
+	defer r.Unlock()
+	if r.configured {
+		return
+	}
 
-		if len(arguments) >= 2 {
-			r.blacklist = getList(arguments[1], "blacklist")
-		}
+	r.configured = true
+	if len(arguments) >= 1 {
+		r.whitelist = getList(arguments[0], "whitelist")
+	}
 
-		if len(arguments) >= 3 {
-			// not pretty code because should keep compatibility with TOML (no mixed array types) and new map parameters
-			thirdArgument := arguments[2]
-			asSlice, ok := thirdArgument.([]interface{})
-			if !ok {
-				panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, got %T", "options", arguments[2]))
-			}
-			if len(asSlice) != 1 {
-				panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, of len==1, but %d", "options", len(asSlice)))
-			}
-			args, ok := asSlice[0].(map[string]interface{})
-			if !ok {
-				panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, of len==1, with map, but %T", "options", asSlice[0]))
-			}
-			r.upperCaseConst = fmt.Sprint(args["upperCaseConst"]) == "true"
+	if len(arguments) >= 2 {
+		r.blacklist = getList(arguments[1], "blacklist")
+	}
+
+	if len(arguments) >= 3 {
+		// not pretty code because should keep compatibility with TOML (no mixed array types) and new map parameters
+		thirdArgument := arguments[2]
+		asSlice, ok := thirdArgument.([]interface{})
+		if !ok {
+			panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, got %T", "options", arguments[2]))
+		}
+		if len(asSlice) != 1 {
+			panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, of len==1, but %d", "options", len(asSlice)))
+		}
+		args, ok := asSlice[0].(map[string]interface{})
+		if !ok {
+			panic(fmt.Sprintf("Invalid third argument to the var-naming rule. Expecting a %s of type slice, of len==1, with map, but %T", "options", asSlice[0]))
 		}
-		r.configured = true
+		r.upperCaseConst = fmt.Sprint(args["upperCaseConst"]) == "true"
 	}
-	r.Unlock()
 }
 
 // Apply applies the rule to given file.


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/
+}
diff --git a/testdata/var-naming_upperCaseConst-false.go b/testdata/var-naming_upperCaseConst-false.go
index 3bb628e..4f56a29 100644
--- a/testdata/var-naming_upperCaseConst-false.go
+++ b/testdata/var-naming_upperCaseConst-false.go
@@ -1,9 +1,11 @@
-// should fail if upperCaseConst = false (by default) #851
+// should fail if upperCaseConst = false (by default) #851, #865
 
 package fixtures
 
-const SOME_CONST_2 = 1 // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
+const SOME_CONST_2 = 1          // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
+const _SOME_PRIVATE_CONST_2 = 2 // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
 
 const (
-	SOME_CONST_3 = 3 // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
+	SOME_CONST_3          = 3 // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
+	_SOME_PRIVATE_CONST_3 = 3 // MATCH /don't use ALL_CAPS in Go names; use CamelCase/
 )
diff --git a/testdata/var-naming_upperCaseConst-true.go b/testdata/var-naming_upperCaseConst-true.go
index 272b708..b459923 100644
--- a/testdata/var-naming_upperCaseConst-true.go
+++ b/testdata/var-naming_upperCaseConst-true.go
@@ -3,8 +3,10 @@
 package fixtures
 
 const SOME_CONST_2 = 2
+const _SOME_PRIVATE_CONST_2 = 2
 
 const (
-	SOME_CONST_3 = 3
-	VER          = 0
+	SOME_CONST_3          = 3
+	_SOME_PRIVATE_CONST_3 = 3
+	VER                   = 0
 )