Repository: mgechev/revive
Pull request #1122 (2024-11-14 11:17:20)

refactor: use toml.Unmarshal instead of toml.Decode
The PR refactors `parseConfig` implementation to use [`toml.Unmarshal`](https://pkg.go.dev/github.com/BurntSushi/toml#Unmarshal) instead of [`toml.Decode`](https://pkg.go.dev/github.com/BurntSushi/toml#Decode). The first accepts the `data` as `[]byte`, the second as `string`. This removes one conversion between from `[]byte` to `string`.

Benchmark says that this refactoring saves one allocation without increasing `parseConfig` complexity.

<details><summary>Benchmarks</summary>
<p>

```go
func BenchmarkParseConfig(b *testing.B) {
	var config lint.Config

	configPath := filepath.Join("..", "full.toml")

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		err := parseConfig(configPath, &config)
		if err != nil {
			b.Fatalf("failed to parse config: %v", err)
		}
	}
}

func BenchmarkParseConfigString(b *testing.B) {
	var config lint.Config

	configPath := filepath.Join("..", "full.toml")

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		err := parseConfigString(configPath, &config)
		if err != nil {
			b.Fatalf("failed to parse config: %v", err)
		}
	}
}
```

```sh
❯ go test -benchmem -bench=. ./...
goos: darwin
goarch: arm64
pkg: github.com/mgechev/revive/config
cpu: Apple M1 Pro
BenchmarkParseConfig-8              5030            213835 ns/op          158632 B/op       1789 allocs/op
BenchmarkParseConfigString-8        5779            210645 ns/op          161789 B/op       1790 allocs/op
PASS
ok      github.com/mgechev/revive/config        2.752s
```

</p>
</details> 


Patch:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index f741224..06e5272 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -521,7 +521,7 @@ Example:
 ```
 
 ## filename-format
-_Description_: enforces conventions on source file names. By default, the rule enforces filenames of the form `^[_A-Za-z0-9][_A-Za-z0-9-]*.go$`: Optionally, the rule can be configured to enforce other forms.
+_Description_: enforces conventions on source file names. By default, the rule enforces filenames of the form `^[_A-Za-z0-9][_A-Za-z0-9-]*\.go$`: Optionally, the rule can be configured to enforce other forms.
 
 _Configuration_: (string) regular expression for source filenames.
 
@@ -529,7 +529,7 @@ Example:
 
 ```toml
 [rule.filename-format]
-    arguments=["^[_a-z][_a-z0-9]*.go$"]
+    arguments=["^[_a-z][_a-z0-9]*\\.go$"]
 ```
 
 ## flag-parameter
diff --git a/config/config.go b/config/config.go
index 16559f5..b8b2360 100644
--- a/config/config.go
+++ b/config/config.go
@@ -165,7 +165,7 @@ func parseConfig(path string, config *lint.Config) error {
 	if err != nil {
 		return errors.New("cannot read the config file")
 	}
-	_, err = toml.Decode(string(file), config)
+	err = toml.Unmarshal(file, config)
 	if err != nil {
 		return fmt.Errorf("cannot parse the config file: %v", err)
 	}
diff --git a/formatter/default.go b/formatter/default.go
index 2d5a044..7af4aad 100644
--- a/formatter/default.go
+++ b/formatter/default.go
@@ -26,3 +26,7 @@ func (*Default) Format(failures <-chan lint.Failure, _ lint.Config) (string, err
 	}
 	return buf.String(), nil
 }
+
+func ruleDescriptionURL(ruleName string) string {
+	return "https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#" + ruleName
+}
diff --git a/formatter/friendly.go b/formatter/friendly.go
index 5ff329a..bbbbcb6 100644
--- a/formatter/friendly.go
+++ b/formatter/friendly.go
@@ -58,8 +58,7 @@ func (f *Friendly) Format(failures <-chan lint.Failure, config lint.Config) (str
 func (f *Friendly) printFriendlyFailure(w io.Writer, failure lint.Failure, severity lint.Severity) {
 	f.printHeaderRow(w, failure, severity)
 	f.printFilePosition(w, failure)
-	fmt.Fprintln(w)
-	fmt.Fprintln(w)
+	fmt.Fprintf(w, "\n\n")
 }
 
 func (f *Friendly) printHeaderRow(w io.Writer, failure lint.Failure, severity lint.Severity) {
@@ -67,7 +66,7 @@ func (f *Friendly) printHeaderRow(w io.Writer, failure lint.Failure, severity li
 	if severity == lint.SeverityError {
 		emoji = getErrorEmoji()
 	}
-	fmt.Fprint(w, f.table([][]string{{emoji, "https://revive.run/r#" + failure.RuleName, color.GreenString(failure.Failure)}}))
+	fmt.Fprint(w, f.table([][]string{{emoji, ruleDescriptionURL(failure.RuleName), color.GreenString(failure.Failure)}}))
 }
 
 func (*Friendly) printFilePosition(w io.Writer, failure lint.Failure) {
@@ -98,13 +97,11 @@ func (*Friendly) printSummary(w io.Writer, errors, warnings int) {
 	}
 	str := fmt.Sprintf("%d %s (%d %s, %d %s)", errors+warnings, problemsLabel, errors, errorsLabel, warnings, warningsLabel)
 	if errors > 0 {
-		fmt.Fprintf(w, "%s %s\n", emoji, color.RedString(str))
-		fmt.Fprintln(w)
+		fmt.Fprintf(w, "%s %s\n\n", emoji, color.RedString(str))
 		return
 	}
 	if warnings > 0 {
-		fmt.Fprintf(w, "%s %s\n", emoji, color.YellowString(str))
-		fmt.Fprintln(w)
+		fmt.Fprintf(w, "%s %s\n\n", emoji, color.YellowString(str))
 		return
 	}
 }
diff --git a/formatter/plain.go b/formatter/plain.go
index 09ebf6c..c972432 100644
--- a/formatter/plain.go
+++ b/formatter/plain.go
@@ -22,7 +22,7 @@ func (*Plain) Name() string {
 func (*Plain) Format(failures <-chan lint.Failure, _ lint.Config) (string, error) {
 	var buf bytes.Buffer
 	for failure := range failures {
-		fmt.Fprintf(&buf, "%v: %s %s\n", failure.Position.Start, failure.Failure, "https://revive.run/r#"+failure.RuleName)
+		fmt.Fprintf(&buf, "%v: %s %s\n", failure.Position.Start, failure.Failure, ruleDescriptionURL(failure.RuleName))
 	}
 	return buf.String(), nil
 }
diff --git a/formatter/sarif.go b/formatter/sarif.go
index c42da73..72da160 100644
--- a/formatter/sarif.go
+++ b/formatter/sarif.go
@@ -20,7 +20,7 @@ func (*Sarif) Name() string {
 	return "sarif"
 }
 
-const reviveSite = "https://revive.run"
+const reviveSite = "https://github.com/mgechev/revive"
 
 // Format formats the failures gotten from the lint.
 func (*Sarif) Format(failures <-chan lint.Failure, cfg lint.Config) (string, error) {
diff --git a/formatter/stylish.go b/formatter/stylish.go
index 828228c..bd821a3 100644
--- a/formatter/stylish.go
+++ b/formatter/stylish.go
@@ -22,11 +22,12 @@ func (*Stylish) Name() string {
 
 func formatFailure(failure lint.Failure, severity lint.Severity) []string {
 	fString := color.CyanString(failure.Failure)
-	fName := color.RedString("https://revive.run/r#" + failure.RuleName)
+	fURL := ruleDescriptionURL(failure.RuleName)
+	fName := color.RedString(fURL)
 	lineColumn := failure.Position
 	pos := fmt.Sprintf("(%d, %d)", lineColumn.Start.Line, lineColumn.Start.Column)
 	if severity == lint.SeverityWarning {
-		fName = color.YellowString("https://revive.run/r#" + failure.RuleName)
+		fName = color.YellowString(fURL)
 	}
 	return []string{failure.GetFilename(), pos, fName, fString}
 }
diff --git a/lint/linter.go b/lint/linter.go
index b42b397..b777f92 100644
--- a/lint/linter.go
+++ b/lint/linter.go
@@ -184,7 +184,9 @@ func detectGoMod(dir string) (rootDir string, ver *goversion.Version, err error)
 func retrieveModFile(dir string) (string, error) {
 	const lookingForFile = "go.mod"
 	for {
-		if dir == "." || dir == "/" {
+		// filepath.Dir returns 'C:\' on Windows, and '/' on Unix
+		isRootDir := (dir == filepath.VolumeName(dir)+string(filepath.Separator))
+		if dir == "." || isRootDir {
 			return "", fmt.Errorf("did not found %q file", lookingForFile)
 		}
 
diff --git a/revive.toml b/revive.toml
index 304909d..298dd71 100644
--- a/revive.toml
+++ b/revive.toml
@@ -21,7 +21,7 @@ warningCode = 1
 [rule.exported]
 [rule.filename-format]
     # Override the default pattern to forbid .go files with uppercase letters and dashes.
-    arguments=["^[_a-z][_a-z0-9]*.go$"]
+    arguments=["^[_a-z][_a-z0-9]*\\.go$"]
 [rule.increment-decrement]
 [rule.indent-error-flow]
 [rule.line-length-limit]
diff --git a/rule/add_constant.go b/rule/add_constant.go
index 233f1d8..399382c 100644
--- a/rule/add_constant.go
+++ b/rule/add_constant.go
@@ -36,12 +36,13 @@ type AddConstantRule struct {
 	allowList       allowList
 	ignoreFunctions []*regexp.Regexp
 	strLitLimit     int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 // Apply applies the rule to given file.
 func (r *AddConstantRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
@@ -201,9 +202,6 @@ func (w *lintAddConstantRule) isStructTag(n *ast.BasicLit) bool {
 }
 
 func (r *AddConstantRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
 	if r.allowList == nil {
 		r.strLitLimit = defaultStrLitLimit
 		r.allowList = newAllowList()
diff --git a/rule/argument_limit.go b/rule/argument_limit.go
index b6ce0e8..b4d56de 100644
--- a/rule/argument_limit.go
+++ b/rule/argument_limit.go
@@ -11,18 +11,13 @@ import (
 // ArgumentsLimitRule lints given else constructs.
 type ArgumentsLimitRule struct {
 	max int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultArgumentsLimit = 8
 
 func (r *ArgumentsLimitRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.max != 0 {
-		return
-	}
-
 	if len(arguments) < 1 {
 		r.max = defaultArgumentsLimit
 		return
@@ -37,7 +32,7 @@ func (r *ArgumentsLimitRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *ArgumentsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/banned_characters.go b/rule/banned_characters.go
index 12997ba..926b32c 100644
--- a/rule/banned_characters.go
+++ b/rule/banned_characters.go
@@ -12,15 +12,14 @@ import (
 // BannedCharsRule checks if a file contains banned characters.
 type BannedCharsRule struct {
 	bannedCharList []string
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const bannedCharsRuleName = "banned-characters"
 
 func (r *BannedCharsRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.bannedCharList == nil && len(arguments) > 0 {
+	if len(arguments) > 0 {
 		checkNumberOfArguments(1, arguments, bannedCharsRuleName)
 		r.bannedCharList = r.getBannedCharsList(arguments)
 	}
@@ -28,7 +27,7 @@ func (r *BannedCharsRule) configure(arguments lint.Arguments) {
 
 // Apply applied the rule to the given file.
 func (r *BannedCharsRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/cognitive_complexity.go b/rule/cognitive_complexity.go
index 83640fd..ecde388 100644
--- a/rule/cognitive_complexity.go
+++ b/rule/cognitive_complexity.go
@@ -13,18 +13,13 @@ import (
 // CognitiveComplexityRule lints given else constructs.
 type CognitiveComplexityRule struct {
 	maxComplexity int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultMaxCognitiveComplexity = 7
 
 func (r *CognitiveComplexityRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.maxComplexity != 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		r.maxComplexity = defaultMaxCognitiveComplexity
 		return
@@ -40,7 +35,7 @@ func (r *CognitiveComplexityRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *CognitiveComplexityRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/comment_spacings.go b/rule/comment_spacings.go
index f721513..7bdc0e7 100644
--- a/rule/comment_spacings.go
+++ b/rule/comment_spacings.go
@@ -12,16 +12,11 @@ import (
 // the comment symbol( // ) and the start of the comment text
 type CommentSpacingsRule struct {
 	allowList []string
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *CommentSpacingsRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.allowList != nil {
-		return // already configured
-	}
-
 	r.allowList = []string{}
 	for _, arg := range arguments {
 		allow, ok := arg.(string) // Alt. non panicking version
@@ -34,7 +29,7 @@ func (r *CommentSpacingsRule) configure(arguments lint.Arguments) {
 
 // Apply the rule.
 func (r *CommentSpacingsRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 
diff --git a/rule/comments_density.go b/rule/comments_density.go
index c5298ea..f2382b1 100644
--- a/rule/comments_density.go
+++ b/rule/comments_density.go
@@ -12,22 +12,13 @@ import (
 // CommentsDensityRule lints given else constructs.
 type CommentsDensityRule struct {
 	minimumCommentsDensity int64
-	configured             bool
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultMinimumCommentsPercentage = 0
 
 func (r *CommentsDensityRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.configured {
-		return
-	}
-
-	r.configured = true
-
 	if len(arguments) < 1 {
 		r.minimumCommentsDensity = defaultMinimumCommentsPercentage
 		return
@@ -42,7 +33,7 @@ func (r *CommentsDensityRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *CommentsDensityRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	commentsLines := countDocLines(file.AST.Comments)
 	statementsCount := countStatements(file.AST)
diff --git a/rule/context_as_argument.go b/rule/context_as_argument.go
index e0c8cfa..8bc5f8b 100644
--- a/rule/context_as_argument.go
+++ b/rule/context_as_argument.go
@@ -12,32 +12,31 @@ import (
 // ContextAsArgumentRule lints given else constructs.
 type ContextAsArgumentRule struct {
 	allowTypesLUT map[string]struct{}
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 // Apply applies the rule to given file.
 func (r *ContextAsArgumentRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.Lock()
-	if r.allowTypesLUT == nil {
-		r.allowTypesLUT = getAllowTypesFromArguments(args)
-	}
-	r.Unlock()
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
-	r.Lock()
 	walker := lintContextArguments{
 		allowTypesLUT: r.allowTypesLUT,
 		onFailure: func(failure lint.Failure) {
 			failures = append(failures, failure)
 		},
 	}
-	r.Unlock()
 
 	ast.Walk(walker, file.AST)
 
 	return failures
 }
 
+func (r *ContextAsArgumentRule) configure(arguments lint.Arguments) {
+	r.allowTypesLUT = getAllowTypesFromArguments(arguments)
+}
+
 // Name returns the rule name.
 func (*ContextAsArgumentRule) Name() string {
 	return "context-as-argument"
diff --git a/rule/cyclomatic.go b/rule/cyclomatic.go
index 10413de..c1a2de9 100644
--- a/rule/cyclomatic.go
+++ b/rule/cyclomatic.go
@@ -14,18 +14,13 @@ import (
 // CyclomaticRule lints given else constructs.
 type CyclomaticRule struct {
 	maxComplexity int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultMaxCyclomaticComplexity = 10
 
 func (r *CyclomaticRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.maxComplexity != 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		r.maxComplexity = defaultMaxCyclomaticComplexity
 		return
@@ -40,7 +35,7 @@ func (r *CyclomaticRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *CyclomaticRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 	fileAst := file.AST
diff --git a/rule/defer.go b/rule/defer.go
index 3c31d50..f7c716e 100644
--- a/rule/defer.go
+++ b/rule/defer.go
@@ -11,22 +11,17 @@ import (
 // DeferRule lints unused params in functions.
 type DeferRule struct {
 	allow map[string]bool
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *DeferRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.allow != nil {
-		return // already configured
-	}
-
 	r.allow = r.allowFromArgs(arguments)
 }
 
 // Apply applies the rule to given file.
 func (r *DeferRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/dot_imports.go b/rule/dot_imports.go
index df0b2a7..f6c7fbc 100644
--- a/rule/dot_imports.go
+++ b/rule/dot_imports.go
@@ -10,13 +10,14 @@ import (
 
 // DotImportsRule lints given else constructs.
 type DotImportsRule struct {
-	sync.Mutex
 	allowedPackages allowPackages
+
+	configureOnce sync.Once
 }
 
 // Apply applies the rule to given file.
 func (r *DotImportsRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
@@ -41,13 +42,6 @@ func (*DotImportsRule) Name() string {
 }
 
 func (r *DotImportsRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.allowedPackages != nil {
-		return
-	}
-
 	r.allowedPackages = make(allowPackages)
 	if len(arguments) == 0 {
 		return
diff --git a/rule/enforce_map_style.go b/rule/enforce_map_style.go
index c698c40..7ddf31e 100644
--- a/rule/enforce_map_style.go
+++ b/rule/enforce_map_style.go
@@ -39,20 +39,12 @@ func mapStyleFromString(s string) (enforceMapStyleType, error) {
 
 // EnforceMapStyleRule implements a rule to enforce `make(map[type]type)` over `map[type]type{}`.
 type EnforceMapStyleRule struct {
-	configured      bool
 	enforceMapStyle enforceMapStyleType
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *EnforceMapStyleRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.configured {
-		return
-	}
-	r.configured = true
-
 	if len(arguments) < 1 {
 		r.enforceMapStyle = enforceMapStyleTypeAny
 		return
@@ -72,7 +64,7 @@ func (r *EnforceMapStyleRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *EnforceMapStyleRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	if r.enforceMapStyle == enforceMapStyleTypeAny {
 		// this linter is not configured
diff --git a/rule/enforce_repeated_arg_type_style.go b/rule/enforce_repeated_arg_type_style.go
index a435ee1..3f9712a 100644
--- a/rule/enforce_repeated_arg_type_style.go
+++ b/rule/enforce_repeated_arg_type_style.go
@@ -41,22 +41,13 @@ func repeatedArgTypeStyleFromString(s string) enforceRepeatedArgTypeStyleType {
 
 // EnforceRepeatedArgTypeStyleRule implements a rule to enforce repeated argument type style.
 type EnforceRepeatedArgTypeStyleRule struct {
-	configured      bool
 	funcArgStyle    enforceRepeatedArgTypeStyleType
 	funcRetValStyle enforceRepeatedArgTypeStyleType
 
-	sync.Mutex
+	configureOnce sync.Once
 }
 
 func (r *EnforceRepeatedArgTypeStyleRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.configured {
-		return
-	}
-	r.configured = true
-
 	r.funcArgStyle = enforceRepeatedArgTypeStyleTypeAny
 	r.funcRetValStyle = enforceRepeatedArgTypeStyleTypeAny
 
@@ -94,7 +85,7 @@ func (r *EnforceRepeatedArgTypeStyleRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to a given file.
 func (r *EnforceRepeatedArgTypeStyleRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	if r.funcArgStyle == enforceRepeatedArgTypeStyleTypeAny && r.funcRetValStyle == enforceRepeatedArgTypeStyleTypeAny {
 		// This linter is not configured, return no failures.
diff --git a/rule/enforce_slice_style.go b/rule/enforce_slice_style.go
index 14be258..7170379 100644
--- a/rule/enforce_slice_style.go
+++ b/rule/enforce_slice_style.go
@@ -43,20 +43,12 @@ func sliceStyleFromString(s string) (enforceSliceStyleType, error) {
 
 // EnforceSliceStyleRule implements a rule to enforce `make([]type)` over `[]type{}`.
 type EnforceSliceStyleRule struct {
-	configured        bool
 	enforceSliceStyle enforceSliceStyleType
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *EnforceSliceStyleRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.configured {
-		return
-	}
-	r.configured = true
-
 	if len(arguments) < 1 {
 		r.enforceSliceStyle = enforceSliceStyleTypeAny
 		return
@@ -76,7 +68,7 @@ func (r *EnforceSliceStyleRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *EnforceSliceStyleRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	if r.enforceSliceStyle == enforceSliceStyleTypeAny {
 		// this linter is not configured
diff --git a/rule/error_strings.go b/rule/error_strings.go
index 81ebda5..97a0f4d 100644
--- a/rule/error_strings.go
+++ b/rule/error_strings.go
@@ -15,17 +15,11 @@ import (
 // ErrorStringsRule lints given else constructs.
 type ErrorStringsRule struct {
 	errorFunctions map[string]map[string]struct{}
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *ErrorStringsRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.errorFunctions != nil {
-		return
-	}
-
 	r.errorFunctions = map[string]map[string]struct{}{
 		"fmt": {
 			"Errorf": {},
@@ -60,7 +54,7 @@ func (r *ErrorStringsRule) configure(arguments lint.Arguments) {
 func (r *ErrorStringsRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	fileAst := file.AST
 	walker := lintErrorStrings{
diff --git a/rule/exported.go b/rule/exported.go
index e3972d4..7ee27b3 100644
--- a/rule/exported.go
+++ b/rule/exported.go
@@ -55,20 +55,13 @@ func (dc *disabledChecks) isDisabled(checkName string) bool {
 
 // ExportedRule lints given else constructs.
 type ExportedRule struct {
-	configured     bool
 	stuttersMsg    string
 	disabledChecks disabledChecks
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *ExportedRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.configured {
-		return
-	}
-	r.configured = true
-
 	r.disabledChecks = disabledChecks{PrivateReceivers: true, PublicInterfaces: true}
 	r.stuttersMsg = "stutters"
 	for _, flag := range arguments {
@@ -104,7 +97,7 @@ func (r *ExportedRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *ExportedRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 	if file.IsTest() {
diff --git a/rule/file_header.go b/rule/file_header.go
index 0dcb577..52513d8 100644
--- a/rule/file_header.go
+++ b/rule/file_header.go
@@ -11,7 +11,8 @@ import (
 // FileHeaderRule lints given else constructs.
 type FileHeaderRule struct {
 	header string
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 var (
@@ -20,12 +21,6 @@ var (
 )
 
 func (r *FileHeaderRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.header != "" {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		return
 	}
@@ -39,7 +34,7 @@ func (r *FileHeaderRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *FileHeaderRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	if r.header == "" {
 		return nil
diff --git a/rule/file_length_limit.go b/rule/file_length_limit.go
index c5a5641..0fe075c 100644
--- a/rule/file_length_limit.go
+++ b/rule/file_length_limit.go
@@ -20,12 +20,13 @@ type FileLengthLimitRule struct {
 	skipComments bool
 	// skipBlankLines indicates whether to skip blank lines when counting lines.
 	skipBlankLines bool
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 // Apply applies the rule to given file.
 func (r *FileLengthLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	if r.max <= 0 {
 		// when max is negative or 0 the rule is disabled
@@ -75,13 +76,6 @@ func (r *FileLengthLimitRule) Apply(file *lint.File, arguments lint.Arguments) [
 }
 
 func (r *FileLengthLimitRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.max != 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		return // use default
 	}
diff --git a/rule/filename_format.go b/rule/filename_format.go
index 49fdf9c..dbdb42d 100644
--- a/rule/filename_format.go
+++ b/rule/filename_format.go
@@ -13,12 +13,13 @@ import (
 // FilenameFormatRule lints source filenames according to a set of regular expressions given as arguments
 type FilenameFormatRule struct {
 	format *regexp.Regexp
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 // Apply applies the rule to the given file.
 func (r *FilenameFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	filename := filepath.Base(file.Name)
 	if r.format.MatchString(filename) {
@@ -52,16 +53,9 @@ func (*FilenameFormatRule) Name() string {
 	return "filename-format"
 }
 
-var defaultFormat = regexp.MustCompile("^[_A-Za-z0-9][_A-Za-z0-9-]*.go$")
+var defaultFormat = regexp.MustCompile(`^[_A-Za-z0-9][_A-Za-z0-9-]*\.go$`)
 
 func (r *FilenameFormatRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.format != nil {
-		return
-	}
-
 	argsCount := len(arguments)
 	if argsCount == 0 {
 		r.format = defaultFormat
diff --git a/rule/function_length.go b/rule/function_length.go
index 3040231..c58cd4c 100644
--- a/rule/function_length.go
+++ b/rule/function_length.go
@@ -11,20 +11,13 @@ import (
 
 // FunctionLength lint.
 type FunctionLength struct {
-	maxStmt    int
-	maxLines   int
-	configured bool
-	sync.Mutex
+	maxStmt  int
+	maxLines int
+
+	configureOnce sync.Once
 }
 
 func (r *FunctionLength) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.configured {
-		return
-	}
-
-	r.configured = true
 	maxStmt, maxLines := r.parseArguments(arguments)
 	r.maxStmt = int(maxStmt)
 	r.maxLines = int(maxLines)
@@ -32,7 +25,7 @@ func (r *FunctionLength) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *FunctionLength) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/function_result_limit.go b/rule/function_result_limit.go
index 23474b5..5b72f01 100644
--- a/rule/function_result_limit.go
+++ b/rule/function_result_limit.go
@@ -11,18 +11,13 @@ import (
 // FunctionResultsLimitRule lints given else constructs.
 type FunctionResultsLimitRule struct {
 	max int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultResultsLimit = 3
 
 func (r *FunctionResultsLimitRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.max != 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		r.max = defaultResultsLimit
 		return
@@ -41,7 +36,7 @@ func (r *FunctionResultsLimitRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *FunctionResultsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/import_alias_naming.go b/rule/import_alias_naming.go
index 48d2256..043bf0d 100644
--- a/rule/import_alias_naming.go
+++ b/rule/import_alias_naming.go
@@ -10,10 +10,10 @@ import (
 
 // ImportAliasNamingRule lints import alias naming.
 type ImportAliasNamingRule struct {
-	configured  bool
 	allowRegexp *regexp.Regexp
 	denyRegexp  *regexp.Regexp
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultImportAliasNamingAllowRule = "^[a-z][a-z0-9]{0,}$"
@@ -21,12 +21,6 @@ const defaultImportAliasNamingAllowRule = "^[a-z][a-z0-9]{0,}$"
 var defaultImportAliasNamingAllowRegexp = regexp.MustCompile(defaultImportAliasNamingAllowRule)
 
 func (r *ImportAliasNamingRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.configured {
-		return
-	}
-
 	if len(arguments) == 0 {
 		r.allowRegexp = defaultImportAliasNamingAllowRegexp
 		return
@@ -57,7 +51,7 @@ func (r *ImportAliasNamingRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *ImportAliasNamingRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/imports_blocklist.go b/rule/imports_blocklist.go
index 4310664..18d77ca 100644
--- a/rule/imports_blocklist.go
+++ b/rule/imports_blocklist.go
@@ -11,29 +11,24 @@ import (
 // ImportsBlocklistRule lints given else constructs.
 type ImportsBlocklistRule struct {
 	blocklist []*regexp.Regexp
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 var replaceImportRegexp = regexp.MustCompile(`/?\*\*/?`)
 
 func (r *ImportsBlocklistRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.blocklist == nil {
-		r.blocklist = make([]*regexp.Regexp, 0)
-
-		for _, arg := range arguments {
-			argStr, ok := arg.(string)
-			if !ok {
-				panic(fmt.Sprintf("Invalid argument to the imports-blocklist rule. Expecting a string, got %T", arg))
-			}
-			regStr, err := regexp.Compile(fmt.Sprintf(`(?m)"%s"$`, replaceImportRegexp.ReplaceAllString(argStr, `(\W|\w)*`)))
-			if err != nil {
-				panic(fmt.Sprintf("Invalid argument to the imports-blocklist rule. Expecting %q to be a valid regular expression, got: %v", argStr, err))
-			}
-			r.blocklist = append(r.blocklist, regStr)
+	r.blocklist = []*regexp.Regexp{}
+	for _, arg := range arguments {
+		argStr, ok := arg.(string)
+		if !ok {
+			panic(fmt.Sprintf("Invalid argument to the imports-blocklist rule. Expecting a string, got %T", arg))
+		}
+		regStr, err := regexp.Compile(fmt.Sprintf(`(?m)"%s"$`, replaceImportRegexp.ReplaceAllString(argStr, `(\W|\w)*`)))
+		if err != nil {
+			panic(fmt.Sprintf("Invalid argument to the imports-blocklist rule. Expecting %q to be a valid regular expression, got: %v", argStr, err))
 		}
+		r.blocklist = append(r.blocklist, regStr)
 	}
 }
 
@@ -48,7 +43,7 @@ func (r *ImportsBlocklistRule) isBlocklisted(path string) bool {
 
 // Apply applies the rule to given file.
 func (r *ImportsBlocklistRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/line_length_limit.go b/rule/line_length_limit.go
index a154b7a..415761e 100644
--- a/rule/line_length_limit.go
+++ b/rule/line_length_limit.go
@@ -15,18 +15,13 @@ import (
 // LineLengthLimitRule lints given else constructs.
 type LineLengthLimitRule struct {
 	max int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultLineLengthLimit = 80
 
 func (r *LineLengthLimitRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.max != 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		r.max = defaultLineLengthLimit
 		return
@@ -42,7 +37,7 @@ func (r *LineLengthLimitRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *LineLengthLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
diff --git a/rule/max_control_nesting.go b/rule/max_control_nesting.go
index 5dbb1ee..b2c5af7 100644
--- a/rule/max_control_nesting.go
+++ b/rule/max_control_nesting.go
@@ -11,14 +11,15 @@ import (
 // MaxControlNestingRule lints given else constructs.
 type MaxControlNestingRule struct {
 	max int64
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultMaxControlNesting = 5
 
 // Apply applies the rule to given file.
 func (r *MaxControlNestingRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
@@ -107,12 +108,6 @@ func (w *lintMaxControlNesting) walkControlledBlock(b ast.Node) {
 }
 
 func (r *MaxControlNestingRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if !(r.max < 1) {
-		return // max already configured
-	}
-
 	if len(arguments) < 1 {
 		r.max = defaultMaxControlNesting
 		return
diff --git a/rule/max_public_structs.go b/rule/max_public_structs.go
index 70840e7..d6f91e3 100644
--- a/rule/max_public_structs.go
+++ b/rule/max_public_structs.go
@@ -1,6 +1,7 @@
 package rule
 
 import (
+	"fmt"
 	"go/ast"
 	"strings"
 	"sync"
@@ -11,18 +12,13 @@ import (
 // MaxPublicStructsRule lints given else constructs.
 type MaxPublicStructsRule struct {
 	max int64
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultMaxPublicStructs = 5
 
 func (r *MaxPublicStructsRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.max == 0 {
-		return // already configured
-	}
-
 	if len(arguments) < 1 {
 		r.max = defaultMaxPublicStructs
 		return
@@ -39,10 +35,14 @@ func (r *MaxPublicStructsRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *MaxPublicStructsRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 
+	if r.max < 1 {
+		return failures
+	}
+
 	fileAst := file.AST
 
 	walker := &lintMaxPublicStructs{
@@ -56,7 +56,7 @@ func (r *MaxPublicStructsRule) Apply(file *lint.File, arguments lint.Arguments)
 
 	if walker.current > r.max {
 		walker.onFailure(lint.Failure{
-			Failure:    "you have exceeded the maximum number of public struct declarations",
+			Failure:    fmt.Sprintf("you have exceeded the maximum number (%d) of public struct declarations", r.max),
 			Confidence: 1,
 			Node:       fileAst,
 			Category:   "style",
diff --git a/rule/modifies_value_receiver.go b/rule/modifies_value_receiver.go
index e9e64b9..2f92991 100644
--- a/rule/modifies_value_receiver.go
+++ b/rule/modifies_value_receiver.go
@@ -2,6 +2,7 @@ package rule
 
 import (
 	"go/ast"
+	"go/token"
 	"strings"
 
 	"github.com/mgechev/revive/lint"
@@ -60,14 +61,14 @@ func (w lintModifiesValRecRule) Visit(node ast.Node) ast.Visitor {
 			return nil // skip, anonymous receiver
 		}
 
-		fselect := func(n ast.Node) bool {
+		receiverAssignmentFinder := func(n ast.Node) bool {
 			// look for assignments with the receiver in the right hand
-			asgmt, ok := n.(*ast.AssignStmt)
+			assignment, ok := n.(*ast.AssignStmt)
 			if !ok {
 				return false
 			}
 
-			for _, exp := range asgmt.Lhs {
+			for _, exp := range assignment.Lhs {
 				switch e := exp.(type) {
 				case *ast.IndexExpr: // receiver...[] = ...
 					continue
@@ -92,7 +93,15 @@ func (w lintModifiesValRecRule) Visit(node ast.Node) ast.Visitor {
 			return false
 		}
 
-		assignmentsToReceiver := pick(n.Body, fselect)
+		assignmentsToReceiver := pick(n.Body, receiverAssignmentFinder)
+		if len(assignmentsToReceiver) == 0 {
+			return nil // receiver is not modified
+		}
+
+		methodReturnsReceiver := len(w.findReturnReceiverStatements(receiverName, n.Body)) > 0
+		if methodReturnsReceiver {
+			return nil // modification seems legit (see issue #1066)
+		}
 
 		for _, assignment := range assignmentsToReceiver {
 			w.onFailure(lint.Failure{
@@ -127,3 +136,44 @@ func (lintModifiesValRecRule) getNameFromExpr(ie ast.Expr) string {
 
 	return ident.Name
 }
+
+func (w lintModifiesValRecRule) findReturnReceiverStatements(receiverName string, target ast.Node) []ast.Node {
+	finder := func(n ast.Node) bool {
+		// look for returns with the receiver as value
+		returnStatement, ok := n.(*ast.ReturnStmt)
+		if !ok {
+			return false
+		}
+
+		for _, exp := range returnStatement.Results {
+			switch e := exp.(type) {
+			case *ast.SelectorExpr: // receiver.field = ...
+				name := w.getNameFromExpr(e.X)
+				if name == "" || name != receiverName {
+					continue
+				}
+			case *ast.Ident: // receiver := ...
+				if e.Name != receiverName {
+					continue
+				}
+			case *ast.UnaryExpr:
+				if e.Op != token.AND {
+					continue
+				}
+				name := w.getNameFromExpr(e.X)
+				if name == "" || name != receiverName {
+					continue
+				}
+
+			default:
+				continue
+			}
+
+			return true
+		}
+
+		return false
+	}
+
+	return pick(target, finder)
+}
diff --git a/rule/receiver_naming.go b/rule/receiver_naming.go
index afcd99b..c83bacc 100644
--- a/rule/receiver_naming.go
+++ b/rule/receiver_naming.go
@@ -12,18 +12,13 @@ import (
 // ReceiverNamingRule lints given else constructs.
 type ReceiverNamingRule struct {
 	receiverNameMaxLength int
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 const defaultReceiverNameMaxLength = -1 // thus will not check
 
 func (r *ReceiverNamingRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.receiverNameMaxLength != 0 {
-		return
-	}
-
 	r.receiverNameMaxLength = defaultReceiverNameMaxLength
 	if len(arguments) < 1 {
 		return
@@ -50,7 +45,7 @@ func (r *ReceiverNamingRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *ReceiverNamingRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 
diff --git a/rule/struct_tag.go b/rule/struct_tag.go
index ec3f0c7..4dd9278 100644
--- a/rule/struct_tag.go
+++ b/rule/struct_tag.go
@@ -14,15 +14,12 @@ import (
 // StructTagRule lints struct tags.
 type StructTagRule struct {
 	userDefined map[string][]string // map: key -> []option
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *StructTagRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	mustConfigure := r.userDefined == nil && len(arguments) > 0
-	if !mustConfigure {
+	if len(arguments) == 0 {
 		return
 	}
 
@@ -47,7 +44,7 @@ func (r *StructTagRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *StructTagRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/unchecked_type_assertion.go b/rule/unchecked_type_assertion.go
index eea3440..34d854e 100644
--- a/rule/unchecked_type_assertion.go
+++ b/rule/unchecked_type_assertion.go
@@ -15,21 +15,16 @@ const (
 
 // UncheckedTypeAssertionRule lints missing or ignored `ok`-value in dynamic type casts.
 type UncheckedTypeAssertionRule struct {
-	sync.Mutex
 	acceptIgnoredAssertionResult bool
-	configured                   bool
-}
 
-func (u *UncheckedTypeAssertionRule) configure(arguments lint.Arguments) {
-	u.Lock()
-	defer u.Unlock()
+	configureOnce sync.Once
+}
 
-	if len(arguments) == 0 || u.configured {
+func (r *UncheckedTypeAssertionRule) configure(arguments lint.Arguments) {
+	if len(arguments) == 0 {
 		return
 	}
 
-	u.configured = true
-
 	args, ok := arguments[0].(map[string]any)
 	if !ok {
 		panic("Unable to get arguments. Expected object of key-value-pairs.")
@@ -38,7 +33,7 @@ func (u *UncheckedTypeAssertionRule) configure(arguments lint.Arguments) {
 	for k, v := range args {
 		switch k {
 		case "acceptIgnoredAssertionResult":
-			u.acceptIgnoredAssertionResult, ok = v.(bool)
+			r.acceptIgnoredAssertionResult, ok = v.(bool)
 			if !ok {
 				panic(fmt.Sprintf("Unable to parse argument '%s'. Expected boolean.", k))
 			}
@@ -49,8 +44,8 @@ func (u *UncheckedTypeAssertionRule) configure(arguments lint.Arguments) {
 }
 
 // Apply applies the rule to given file.
-func (u *UncheckedTypeAssertionRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	u.configure(args)
+func (r *UncheckedTypeAssertionRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 
@@ -58,7 +53,7 @@ func (u *UncheckedTypeAssertionRule) Apply(file *lint.File, args lint.Arguments)
 		onFailure: func(failure lint.Failure) {
 			failures = append(failures, failure)
 		},
-		acceptIgnoredTypeAssertionResult: u.acceptIgnoredAssertionResult,
+		acceptIgnoredTypeAssertionResult: r.acceptIgnoredAssertionResult,
 	}
 
 	ast.Walk(walker, file.AST)
diff --git a/rule/unhandled_error.go b/rule/unhandled_error.go
index 95ba561..4fad8cc 100644
--- a/rule/unhandled_error.go
+++ b/rule/unhandled_error.go
@@ -14,17 +14,11 @@ import (
 // UnhandledErrorRule lints given else constructs.
 type UnhandledErrorRule struct {
 	ignoreList []*regexp.Regexp
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *UnhandledErrorRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-
-	if r.ignoreList != nil {
-		return // already configured
-	}
-
 	for _, arg := range arguments {
 		argStr, ok := arg.(string)
 		if !ok {
@@ -47,7 +41,7 @@ func (r *UnhandledErrorRule) configure(arguments lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *UnhandledErrorRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
 
diff --git a/rule/unused_param.go b/rule/unused_param.go
index 4b04ee9..a8514ac 100644
--- a/rule/unused_param.go
+++ b/rule/unused_param.go
@@ -11,22 +11,14 @@ import (
 
 // UnusedParamRule lints unused params in functions.
 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
+
+	configureOnce sync.Once
 }
 
 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
@@ -58,7 +50,7 @@ func (r *UnusedParamRule) configure(args lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *UnusedParamRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 	var failures []lint.Failure
 
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/unused_receiver.go b/rule/unused_receiver.go
index 715dba3..131aae5 100644
--- a/rule/unused_receiver.go
+++ b/rule/unused_receiver.go
@@ -11,22 +11,14 @@ import (
 
 // UnusedReceiverRule lints unused params in functions.
 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
+
+	configureOnce sync.Once
 }
 
 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
@@ -57,7 +49,7 @@ func (r *UnusedReceiverRule) configure(args lint.Arguments) {
 
 // Apply applies the rule to given file.
 func (r *UnusedReceiverRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
-	r.configure(args)
+	r.configureOnce.Do(func() { r.configure(args) })
 	var failures []lint.Failure
 
 	onFailure := func(failure lint.Failure) {
diff --git a/rule/var_naming.go b/rule/var_naming.go
index 5a4d0dc..2c2198d 100644
--- a/rule/var_naming.go
+++ b/rule/var_naming.go
@@ -18,22 +18,15 @@ var upperCaseConstRE = regexp.MustCompile(`^_?[A-Z][A-Z\d]*(_[A-Z\d]+)*$`)
 
 // VarNamingRule lints given else constructs.
 type VarNamingRule struct {
-	configured            bool
 	allowList             []string
 	blockList             []string
 	allowUpperCaseConst   bool // if true - allows to use UPPER_SOME_NAMES for constants
 	skipPackageNameChecks bool
-	sync.Mutex
+
+	configureOnce sync.Once
 }
 
 func (r *VarNamingRule) configure(arguments lint.Arguments) {
-	r.Lock()
-	defer r.Unlock()
-	if r.configured {
-		return
-	}
-
-	r.configured = true
 	if len(arguments) >= 1 {
 		r.allowList = getList(arguments[0], "allowlist")
 	}
@@ -83,7 +76,7 @@ func (r *VarNamingRule) applyPackageCheckRules(walker *lintNames) {
 
 // Apply applies the rule to given file.
 func (r *VarNamingRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
-	r.configure(arguments)
+	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
 


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index a897ef7..5800b39 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -5,8 +5,8 @@ on:
     types: [opened, edited, synchronize, reopened]
 
 jobs:
-  test:
-    name: Test
+  tests-on-unix:
+    name: Tests on Unix
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
@@ -23,3 +23,16 @@ jobs:
           go-version: ${{ matrix.go-version }}
       - name: Run tests
         run: go test -race -shuffle=on ./...
+
+  tests-on-windows:
+    name: Tests on Windows
+    runs-on: windows-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: stable
+      - name: Run tests
+        run: go test -race -shuffle=on ./...
diff --git a/cli/main_test.go b/cli/main_test.go
index c570efd..fd3a5ba 100644
--- a/cli/main_test.go
+++ b/cli/main_test.go
@@ -2,6 +2,7 @@ package cli
 
 import (
 	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/mitchellh/go-homedir"
@@ -22,19 +23,19 @@ func TestXDGConfigDirIsPreferredFirst(t *testing.T) {
 		AppFs = afero.NewMemMapFs()
 	})
 
-	xdgDirPath := "/tmp-iofs/xdg/config"
-	homeDirPath := "/tmp-iofs/home/tester"
+	xdgDirPath := filepath.FromSlash("/tmp-iofs/xdg/config")
+	homeDirPath := filepath.FromSlash("/tmp-iofs/home/tester")
 	AppFs.MkdirAll(xdgDirPath, 0755)
 	AppFs.MkdirAll(homeDirPath, 0755)
 
-	afero.WriteFile(AppFs, xdgDirPath+"/revive.toml", []byte("\n"), 0644)
+	afero.WriteFile(AppFs, filepath.Join(xdgDirPath, "revive.toml"), []byte("\n"), 0644)
 	t.Setenv("XDG_CONFIG_HOME", xdgDirPath)
 
-	afero.WriteFile(AppFs, homeDirPath+"/revive.toml", []byte("\n"), 0644)
+	afero.WriteFile(AppFs, filepath.Join(homeDirPath, "revive.toml"), []byte("\n"), 0644)
 	t.Setenv("HOME", homeDirPath)
 
 	got := buildDefaultConfigPath()
-	want := xdgDirPath + "/revive.toml"
+	want := filepath.Join(xdgDirPath, "revive.toml")
 
 	if got != want {
 		t.Errorf("got %q, wanted %q", got, want)
@@ -43,14 +44,14 @@ func TestXDGConfigDirIsPreferredFirst(t *testing.T) {
 
 func TestHomeConfigDir(t *testing.T) {
 	t.Cleanup(func() { AppFs = afero.NewMemMapFs() })
-	homeDirPath := "/tmp-iofs/home/tester"
+	homeDirPath := filepath.FromSlash("/tmp-iofs/home/tester")
 	AppFs.MkdirAll(homeDirPath, 0755)
 
-	afero.WriteFile(AppFs, homeDirPath+"/revive.toml", []byte("\n"), 0644)
+	afero.WriteFile(AppFs, filepath.Join(homeDirPath, "revive.toml"), []byte("\n"), 0644)
 	t.Setenv("HOME", homeDirPath)
 
 	got := buildDefaultConfigPath()
-	want := homeDirPath + "/revive.toml"
+	want := filepath.Join(homeDirPath, "revive.toml")
 
 	if got != want {
 		t.Errorf("got %q, wanted %q", got, want)
@@ -59,14 +60,14 @@ func TestHomeConfigDir(t *testing.T) {
 
 func TestXDGConfigDir(t *testing.T) {
 	t.Cleanup(func() { AppFs = afero.NewMemMapFs() })
-	xdgDirPath := "/tmp-iofs/xdg/config"
+	xdgDirPath := filepath.FromSlash("/tmp-iofs/xdg/config")
 	AppFs.MkdirAll(xdgDirPath, 0755)
 
-	afero.WriteFile(AppFs, xdgDirPath+"/revive.toml", []byte("\n"), 0644)
+	afero.WriteFile(AppFs, filepath.Join(xdgDirPath, "revive.toml"), []byte("\n"), 0644)
 	t.Setenv("XDG_CONFIG_HOME", xdgDirPath)
 
 	got := buildDefaultConfigPath()
-	want := xdgDirPath + "/revive.toml"
+	want := filepath.Join(xdgDirPath, "revive.toml")
 
 	if got != want {
 		t.Errorf("got %q, wanted %q", got, want)
@@ -75,7 +76,7 @@ func TestXDGConfigDir(t *testing.T) {
 
 func TestXDGConfigDirNoFile(t *testing.T) {
 	t.Cleanup(func() { AppFs = afero.NewMemMapFs() })
-	xdgDirPath := "/tmp-iofs/xdg/config"
+	xdgDirPath := filepath.FromSlash("/tmp-iofs/xdg/config")
 	t.Setenv("XDG_CONFIG_HOME", xdgDirPath)
 
 	got := buildDefaultConfigPath()
diff --git a/formatter/formatter_test.go b/formatter/formatter_test.go
index e0d4e9d..89f46b1 100644
--- a/formatter/formatter_test.go
+++ b/formatter/formatter_test.go
@@ -50,7 +50,7 @@ func TestFormatter(t *testing.T) {
 		{
 			formatter: &formatter.Friendly{},
 			want: `
-⚠  https://revive.run/r#rule  test failure  
+⚠  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#rule  test failure  
   test.go:2:5
 
 ⚠ 1 problem (0 errors, 1 warning)
@@ -69,7 +69,7 @@ Warnings:
 		},
 		{
 			formatter: &formatter.Plain{},
-			want:      `test.go:2:5: test failure https://revive.run/r#rule`,
+			want:      `test.go:2:5: test failure https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#rule`,
 		},
 		{
 			formatter: &formatter.Sarif{},
@@ -100,7 +100,7 @@ Warnings:
       ],
       "tool": {
         "driver": {
-          "informationUri": "https://revive.run",
+          "informationUri": "https://github.com/mgechev/revive",
           "name": "revive"
         }
       }
@@ -114,7 +114,7 @@ Warnings:
 			formatter: &formatter.Stylish{},
 			want: `
 test.go
-  (2, 5)  https://revive.run/r#rule  test failure  
+  (2, 5)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#rule  test failure  
 
 
  ✖ 1 problem (0 errors) (1 warnings)
diff --git a/lint/linter_test.go b/lint/linter_test.go
index 4c1f95d..a7bb1d2 100644
--- a/lint/linter_test.go
+++ b/lint/linter_test.go
@@ -1,6 +1,44 @@
 package lint
 
-import "testing"
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestRetrieveModFile(t *testing.T) {
+	t.Run("go.mod file exists", func(t *testing.T) {
+		nestedDir := filepath.Join(t.TempDir(), "nested", "dir", "structure")
+		err := os.MkdirAll(nestedDir, 0o755)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		modFilePath := filepath.Join(nestedDir, "go.mod")
+		err = os.WriteFile(modFilePath, []byte("module example.com/test"), 0o644)
+		if err != nil {
+			t.Fatal(err)
+		}
+		foundPath, err := retrieveModFile(nestedDir)
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+		if foundPath != modFilePath {
+			t.Fatalf("expected %q, got %q", modFilePath, foundPath)
+		}
+	})
+
+	t.Run("go.mod file does not exist", func(t *testing.T) {
+		_, err := retrieveModFile(t.TempDir())
+		if err == nil {
+			t.Fatalf("expected error, got nil")
+		}
+		expectedErrMsg := `did not found "go.mod" file`
+		if err.Error() != expectedErrMsg {
+			t.Fatalf("expected error message %q, got %q", expectedErrMsg, err.Error())
+		}
+	})
+}
 
 // TestIsGenerated tests isGenerated function.
 func TestIsGenerated(t *testing.T) { //revive:disable-line:exported
diff --git a/revivelib/core_test.go b/revivelib/core_test.go
index 10670c7..60ed94b 100644
--- a/revivelib/core_test.go
+++ b/revivelib/core_test.go
@@ -54,11 +54,11 @@ func TestReviveFormat(t *testing.T) {
 	}
 
 	errorMsgs := []string{
-		"(91, 3)  https://revive.run/r#unreachable-code  unreachable code after this statement",
-		"(98, 3)  https://revive.run/r#unreachable-code  unreachable code after this statement",
-		"(15, 2)  https://revive.run/r#if-return         redundant if ...; err != nil check, just return error instead.",
-		"(88, 3)  https://revive.run/r#if-return         redundant if ...; err != nil check, just return error instead.",
-		"(95, 3)  https://revive.run/r#if-return         redundant if ...; err != nil check, just return error instead.",
+		"(91, 3)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code  unreachable code after this statement",
+		"(98, 3)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code  unreachable code after this statement",
+		"(15, 2)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return         redundant if ...; err != nil check, just return error instead.",
+		"(88, 3)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return         redundant if ...; err != nil check, just return error instead.",
+		"(95, 3)  https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return         redundant if ...; err != nil check, just return error instead.",
 	}
 	for _, errorMsg := range errorMsgs {
 		if !strings.Contains(failures, errorMsg) {
diff --git a/test/max_public_structs_test.go b/test/max_public_structs_test.go
index 22c7455..55718b0 100644
--- a/test/max_public_structs_test.go
+++ b/test/max_public_structs_test.go
@@ -12,3 +12,7 @@ func TestMaxPublicStructs(t *testing.T) {
 		Arguments: []any{int64(1)},
 	})
 }
+
+func TestMaxPublicStructsDefaultConfig(t *testing.T) {
+	testRule(t, "max_public_structs_ok", &rule.MaxPublicStructsRule{}, &lint.RuleConfig{})
+}
diff --git "a/testdata/filenam\320\265_with_non_ascii_char.go" "b/testdata/filenam\320\265_with_non_ascii_char.go"
index d6cf886..d70d2bf 100644
--- "a/testdata/filenam\320\265_with_non_ascii_char.go"
+++ "b/testdata/filenam\320\265_with_non_ascii_char.go"
@@ -1,3 +1,3 @@
 package main
 
-// MATCH:1 /Filename filenamе_with_non_ascii_char.go is not of the format ^[_A-Za-z0-9][_A-Za-z0-9-]*.go$. Non ASCII character е (U+0435) found./
+// MATCH:1 /Filename filenamе_with_non_ascii_char.go is not of the format ^[_A-Za-z0-9][_A-Za-z0-9-]*\.go$. Non ASCII character е (U+0435) found./
diff --git a/testdata/max_public_structs.go b/testdata/max_public_structs.go
index abdd3b7..9188c5f 100644
--- a/testdata/max_public_structs.go
+++ b/testdata/max_public_structs.go
@@ -1,5 +1,5 @@
 // Package pkg ...
-package pkg // MATCH /you have exceeded the maximum number of public struct declarations/
+package pkg // MATCH /you have exceeded the maximum number (1) of public struct declarations/
 
 type Foo struct {
 }
diff --git a/testdata/max_public_structs_ok.go b/testdata/max_public_structs_ok.go
new file mode 100644
index 0000000..5aaac52
--- /dev/null
+++ b/testdata/max_public_structs_ok.go
@@ -0,0 +1,11 @@
+// Package pkg ...
+package pkg
+
+type Foo struct {
+}
+
+type Bar struct {
+}
+
+type Baz struct {
+}
diff --git a/testdata/modifies_value_receiver.go b/testdata/modifies_value_receiver.go
index b5ae877..e6a7166 100644
--- a/testdata/modifies_value_receiver.go
+++ b/testdata/modifies_value_receiver.go
@@ -12,3 +12,19 @@ func (this data) vmethod() {
 	this.items = make(map[string]bool) // MATCH /suspicious assignment to a by-value method receiver/
 	this.items["vmethod"] = true
 }
+
+func (a A) Foo() *A {
+	a.whatever = true
+	return &a
+}
+
+func (a A) Clone() (*A, error) {
+	a.whatever = true
+	return &a, nil
+}
+
+// WithBin will set the specific bin path to the builder.
+func (b JailerCommandBuilder) WithBin(bin string) JailerCommandBuilder {
+	b.bin = bin
+	return b
+}