Repository: mgechev/revive
Pull request #1216 (2025-01-31 14:19:40)

refactor: simplify File.disabledIntervals, add tests
This PR performs some refactoring for the function `File.disabledIntervals` and add basic tests to cover most common cases.

chavacava: Hi @alexandear, thank you for the PR. I left a comment, let me know what do you think. (the PR can be merged as is but I'll wait for your response a couple of days)
alexandear: > Hi @alexandear, thank you for the PR. I left a comment, let me know what do you think. (the PR can be merged as is but I'll wait for your response a couple of days)

@chavacava thank you for the review. I have fixed the comments by adding a utility function. Please re-check.

Sorry for the late response.

Patch:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 0cb3856..ffa74b3 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -456,10 +456,20 @@ _Configuration_: N/A
 ## error-strings
 
 _Description_: By convention, for better readability, error messages should not be capitalized or end with punctuation or a newline.
+By default, the rule analyzes functions for creating errors from `fmt`, `errors`, and `github.com/pkg/errors`.
+Optionally, the rule can be configured to analyze user functions that create errors.
 
 More information [here](https://go.dev/wiki/CodeReviewComments#error-strings)
 
-_Configuration_: N/A
+_Configuration_: ([]string) the list of additional error functions to check.
+The format of values is `package.FunctionName`.
+
+Example:
+
+```toml
+[rule.error-strings]
+  arguments = ["xerrors.Errorf"]
+```
 
 ## errorf
 
@@ -855,7 +865,7 @@ _Configuration_: N/A
 ## struct-tag
 
 _Description_: Struct tags are not checked at compile time.
-This rule, checks and warns if it finds errors in common struct tags types like: asn1, default, json, protobuf, xml, yaml.
+This rule spots errors in struct tags of the following types: asn1, bson, datastore, default, json, mapstructure, protobuf, required, toml, url, validate, xml, yaml.
 
 _Configuration_: (optional) list of user defined options.
 
diff --git a/go.mod b/go.mod
index a5f9a9c..443480d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,8 @@
 module github.com/mgechev/revive
 
-go 1.22.1
+go 1.23.0
+
+toolchain go1.24.1
 
 require (
 	github.com/BurntSushi/toml v1.4.0
@@ -11,9 +13,9 @@ require (
 	github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/afero v1.12.0
-	golang.org/x/mod v0.23.0
-	golang.org/x/sync v0.11.0
-	golang.org/x/tools v0.30.0
+	golang.org/x/mod v0.24.0
+	golang.org/x/sync v0.12.0
+	golang.org/x/tools v0.31.0
 )
 
 require (
@@ -22,6 +24,6 @@ require (
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/stretchr/testify v1.10.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/text v0.23.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 540a130..e77c128 100644
--- a/go.sum
+++ b/go.sum
@@ -37,17 +37,17 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
 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/file.go b/lint/file.go
index 0311210..69828d9 100644
--- a/lint/file.go
+++ b/lint/file.go
@@ -135,15 +135,16 @@ type enableDisableConfig struct {
 	position int
 }
 
+type disabledIntervalsMap = map[string][]DisabledInterval
+
 const (
-	directiveRE  = `^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$`
 	directivePos = 1
 	modifierPos  = 2
 	rulesPos     = 3
 	reasonPos    = 4
 )
 
-var re = regexp.MustCompile(directiveRE)
+var directiveRegexp = regexp.MustCompile(`^//[\s]*revive:(enable|disable)(?:-(line|next-line))?(?::([^\s]+))?[\s]*(?: (.+))?$`)
 
 func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, failures chan Failure) disabledIntervalsMap {
 	enabledDisabledRulesMap := map[string][]enableDisableConfig{}
@@ -194,8 +195,7 @@ func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, fa
 		enabledDisabledRulesMap[name] = existing
 	}
 
-	handleRules := func(_, modifier string, isEnabled bool, line int, ruleNames []string) []DisabledInterval {
-		var result []DisabledInterval
+	handleRules := func(modifier string, isEnabled bool, line int, ruleNames []string) {
 		for _, name := range ruleNames {
 			switch modifier {
 			case "line":
@@ -208,13 +208,12 @@ func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, fa
 				handleConfig(isEnabled, line, name)
 			}
 		}
-		return result
 	}
 
-	handleComment := func(filename string, c *ast.CommentGroup, line int) {
+	handleComment := func(c *ast.CommentGroup, line int) {
 		comments := c.List
 		for _, c := range comments {
-			match := re.FindStringSubmatch(c.Text)
+			match := directiveRegexp.FindStringSubmatch(c.Text)
 			if len(match) == 0 {
 				continue
 			}
@@ -247,13 +246,12 @@ func (f *File) disabledIntervals(rules []Rule, mustSpecifyDisableReason bool, fa
 				}
 			}
 
-			handleRules(filename, match[modifierPos], match[directivePos] == "enable", line, ruleNames)
+			handleRules(match[modifierPos], match[directivePos] == "enable", line, ruleNames)
 		}
 	}
 
-	comments := f.AST.Comments
-	for _, c := range comments {
-		handleComment(f.Name, c, f.ToPosition(c.End()).Line)
+	for _, c := range f.AST.Comments {
+		handleComment(c, f.ToPosition(c.End()).Line)
 	}
 
 	return getEnabledDisabledIntervals()
diff --git a/lint/linter.go b/lint/linter.go
index 73b5341..38f4ae7 100644
--- a/lint/linter.go
+++ b/lint/linter.go
@@ -19,8 +19,6 @@ import (
 // ReadFile defines an abstraction for reading files.
 type ReadFile func(path string) (result []byte, err error)
 
-type disabledIntervalsMap = map[string][]DisabledInterval
-
 // Linter is used for linting set of files.
 type Linter struct {
 	reader         ReadFile
diff --git a/rule/error_strings.go b/rule/error_strings.go
index 839a613..a5c2454 100644
--- a/rule/error_strings.go
+++ b/rule/error_strings.go
@@ -37,14 +37,19 @@ func (r *ErrorStringsRule) Configure(arguments lint.Arguments) error {
 
 	var invalidCustomFunctions []string
 	for _, argument := range arguments {
-		if functionName, ok := argument.(string); ok {
-			fields := strings.Split(strings.TrimSpace(functionName), ".")
-			if len(fields) != 2 || len(fields[0]) == 0 || len(fields[1]) == 0 {
-				invalidCustomFunctions = append(invalidCustomFunctions, functionName)
-				continue
-			}
-			r.errorFunctions[fields[0]] = map[string]struct{}{fields[1]: {}}
+		pkgFunction, ok := argument.(string)
+		if !ok {
+			continue
 		}
+		pkg, function, ok := strings.Cut(strings.TrimSpace(pkgFunction), ".")
+		if !ok || pkg == "" || function == "" {
+			invalidCustomFunctions = append(invalidCustomFunctions, pkgFunction)
+			continue
+		}
+		if _, ok := r.errorFunctions[pkg]; !ok {
+			r.errorFunctions[pkg] = map[string]struct{}{}
+		}
+		r.errorFunctions[pkg][function] = struct{}{}
 	}
 	if len(invalidCustomFunctions) != 0 {
 		return fmt.Errorf("found invalid custom function: %s", strings.Join(invalidCustomFunctions, ","))
diff --git a/rule/string_format.go b/rule/string_format.go
index a3beac4..d539577 100644
--- a/rule/string_format.go
+++ b/rule/string_format.go
@@ -12,20 +12,24 @@ import (
 )
 
 // StringFormatRule lints strings and/or comments according to a set of regular expressions given as Arguments
-type StringFormatRule struct{}
+type StringFormatRule struct {
+	rules []stringFormatSubrule
+}
 
 // Apply applies the rule to the given file.
-func (*StringFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
+func (r *StringFormatRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
 	onFailure := func(failure lint.Failure) {
 		failures = append(failures, failure)
 	}
 
-	w := &lintStringFormatRule{onFailure: onFailure}
-	err := w.parseArguments(arguments)
-	if err != nil {
-		return newInternalFailureError(err)
+	for i := range r.rules {
+		r.rules[i].onFailure = onFailure
+	}
+
+	w := &lintStringFormatRule{
+		rules: r.rules,
 	}
 
 	ast.Walk(w, file.AST)
@@ -38,32 +42,31 @@ func (*StringFormatRule) Name() string {
 	return "string-format"
 }
 
-// ParseArgumentsTest is a public wrapper around w.parseArguments used for testing. Returns the error message provided to panic, or nil if no error was encountered
-func (*StringFormatRule) ParseArgumentsTest(arguments lint.Arguments) *string {
-	w := lintStringFormatRule{}
-	c := make(chan any)
-	// Parse the arguments in a goroutine, defer a recover() call, return the error encountered (or nil if there was no error)
-	go func() {
-		defer func() {
-			err := w.parseArguments(arguments)
-			c <- err
-		}()
-	}()
-	err := <-c
-	if err != nil {
-		e := fmt.Sprintf("%s", err)
-		return &e
+// Configure validates the rule configuration, and configures the rule accordingly.
+//
+// Configuration implements the [lint.ConfigurableRule] interface.
+func (r *StringFormatRule) Configure(arguments lint.Arguments) error {
+	for i, argument := range arguments {
+		scopes, regex, negated, errorMessage, err := r.parseArgument(argument, i)
+		if err != nil {
+			return err
+		}
+		r.rules = append(r.rules, stringFormatSubrule{
+			scopes:       scopes,
+			regexp:       regex,
+			negated:      negated,
+			errorMessage: errorMessage,
+		})
 	}
 	return nil
 }
 
 type lintStringFormatRule struct {
-	onFailure func(lint.Failure)
-	rules     []stringFormatSubrule
+	rules []stringFormatSubrule
 }
 
 type stringFormatSubrule struct {
-	parent       *lintStringFormatRule
+	onFailure    func(lint.Failure)
 	scopes       stringFormatSubruleScopes
 	regexp       *regexp.Regexp
 	negated      bool
@@ -84,45 +87,28 @@ const identRegex = "[_A-Za-z][_A-Za-z0-9]*"
 var parseStringFormatScope = regexp.MustCompile(
 	fmt.Sprintf("^(%s(?:\\.%s)?)(?:\\[([0-9]+)\\](?:\\.(%s))?)?$", identRegex, identRegex, identRegex))
 
-func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) error {
-	for i, argument := range arguments {
-		scopes, regex, negated, errorMessage, err := w.parseArgument(argument, i)
-		if err != nil {
-			return err
-		}
-		w.rules = append(w.rules, stringFormatSubrule{
-			parent:       w,
-			scopes:       scopes,
-			regexp:       regex,
-			negated:      negated,
-			errorMessage: errorMessage,
-		})
-	}
-	return nil
-}
-
-func (w *lintStringFormatRule) parseArgument(argument any, ruleNum int) (scopes stringFormatSubruleScopes, regex *regexp.Regexp, negated bool, errorMessage string, err error) {
+func (r *StringFormatRule) parseArgument(argument any, ruleNum int) (scopes stringFormatSubruleScopes, regex *regexp.Regexp, negated bool, errorMessage string, err error) {
 	g, ok := argument.([]any) // Cast to generic slice first
 	if !ok {
-		return stringFormatSubruleScopes{}, regex, false, "", w.configError("argument is not a slice", ruleNum, 0)
+		return stringFormatSubruleScopes{}, regex, false, "", r.configError("argument is not a slice", ruleNum, 0)
 	}
 	if len(g) < 2 {
-		return stringFormatSubruleScopes{}, regex, false, "", w.configError("less than two slices found in argument, scope and regex are required", ruleNum, len(g)-1)
+		return stringFormatSubruleScopes{}, regex, false, "", r.configError("less than two slices found in argument, scope and regex are required", ruleNum, len(g)-1)
 	}
 	rule := make([]string, len(g))
 	for i, obj := range g {
 		val, ok := obj.(string)
 		if !ok {
-			return stringFormatSubruleScopes{}, regex, false, "", w.configError("unexpected value, string was expected", ruleNum, i)
+			return stringFormatSubruleScopes{}, regex, false, "", r.configError("unexpected value, string was expected", ruleNum, i)
 		}
 		rule[i] = val
 	}
 
 	// Validate scope and regex length
 	if rule[0] == "" {
-		return stringFormatSubruleScopes{}, regex, false, "", w.configError("empty scope provided", ruleNum, 0)
+		return stringFormatSubruleScopes{}, regex, false, "", r.configError("empty scope provided", ruleNum, 0)
 	} else if len(rule[1]) < 2 {
-		return stringFormatSubruleScopes{}, regex, false, "", w.configError("regex is too small (regexes should begin and end with '/')", ruleNum, 1)
+		return stringFormatSubruleScopes{}, regex, false, "", r.configError("regex is too small (regexes should begin and end with '/')", ruleNum, 1)
 	}
 
 	// Parse rule scopes
@@ -133,25 +119,25 @@ func (w *lintStringFormatRule) parseArgument(argument any, ruleNum int) (scopes
 		rawScope = strings.TrimSpace(rawScope)
 
 		if len(rawScope) == 0 {
-			return stringFormatSubruleScopes{}, regex, false, "", w.parseScopeError("empty scope in rule scopes:", ruleNum, 0, scopeNum)
+			return stringFormatSubruleScopes{}, regex, false, "", r.parseScopeError("empty scope in rule scopes:", ruleNum, 0, scopeNum)
 		}
 
 		scope := stringFormatSubruleScope{}
 		matches := parseStringFormatScope.FindStringSubmatch(rawScope)
 		if matches == nil {
 			// The rule's scope didn't match the parsing regex at all, probably a configuration error
-			return stringFormatSubruleScopes{}, regex, false, "", w.parseScopeError("unable to parse rule scope", ruleNum, 0, scopeNum)
+			return stringFormatSubruleScopes{}, regex, false, "", r.parseScopeError("unable to parse rule scope", ruleNum, 0, scopeNum)
 		} else if len(matches) != 4 {
 			// The rule's scope matched the parsing regex, but an unexpected number of submatches was returned, probably a bug
 			return stringFormatSubruleScopes{}, regex, false, "",
-				w.parseScopeError(fmt.Sprintf("unexpected number of submatches when parsing scope: %d, expected 4", len(matches)), ruleNum, 0, scopeNum)
+				r.parseScopeError(fmt.Sprintf("unexpected number of submatches when parsing scope: %d, expected 4", len(matches)), ruleNum, 0, scopeNum)
 		}
 		scope.funcName = matches[1]
 		if len(matches[2]) > 0 {
 			var err error
 			scope.argument, err = strconv.Atoi(matches[2])
 			if err != nil {
-				return stringFormatSubruleScopes{}, regex, false, "", w.parseScopeError("unable to parse argument number in rule scope", ruleNum, 0, scopeNum)
+				return stringFormatSubruleScopes{}, regex, false, "", r.parseScopeError("unable to parse argument number in rule scope", ruleNum, 0, scopeNum)
 			}
 		}
 		if len(matches[3]) > 0 {
@@ -169,7 +155,7 @@ func (w *lintStringFormatRule) parseArgument(argument any, ruleNum int) (scopes
 	}
 	regex, errr := regexp.Compile(rule[1][offset : len(rule[1])-1])
 	if errr != nil {
-		return stringFormatSubruleScopes{}, regex, false, "", w.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1)
+		return stringFormatSubruleScopes{}, regex, false, "", r.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1)
 	}
 
 	// Use custom error message if provided
@@ -180,17 +166,17 @@ func (w *lintStringFormatRule) parseArgument(argument any, ruleNum int) (scopes
 }
 
 // Report an invalid config, this is specifically the user's fault
-func (*lintStringFormatRule) configError(msg string, ruleNum, option int) error {
+func (*StringFormatRule) configError(msg string, ruleNum, option int) error {
 	return fmt.Errorf("invalid configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option)
 }
 
 // Report a general config parsing failure, this may be the user's fault, but it isn't known for certain
-func (*lintStringFormatRule) parseError(msg string, ruleNum, option int) error {
+func (*StringFormatRule) parseError(msg string, ruleNum, option int) error {
 	return fmt.Errorf("failed to parse configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option)
 }
 
 // Report a general scope config parsing failure, this may be the user's fault, but it isn't known for certain
-func (*lintStringFormatRule) parseScopeError(msg string, ruleNum, option, scopeNum int) error {
+func (*StringFormatRule) parseScopeError(msg string, ruleNum, option, scopeNum int) error {
 	return fmt.Errorf("failed to parse configuration for string-format: %s [argument %d, option %d, scope index %d]", msg, ruleNum, option, scopeNum)
 }
 
@@ -314,7 +300,7 @@ func (r *stringFormatSubrule) generateFailure(node ast.Node) {
 		failure = fmt.Sprintf("string literal doesn't match user defined regex /%s/", r.regexp.String())
 	}
 
-	r.parent.onFailure(lint.Failure{
+	r.onFailure(lint.Failure{
 		Confidence: 1,
 		Failure:    failure,
 		Node:       node,
diff --git a/rule/struct_tag.go b/rule/struct_tag.go
index 216ed80..a154c1c 100644
--- a/rule/struct_tag.go
+++ b/rule/struct_tag.go
@@ -98,16 +98,19 @@ func (w lintStructTagRule) Visit(node ast.Node) ast.Visitor {
 }
 
 const (
-	keyASN1      = "asn1"
-	keyBSON      = "bson"
-	keyDatastore = "datastore"
-	keyDefault   = "default"
-	keyJSON      = "json"
-	keyProtobuf  = "protobuf"
-	keyRequired  = "required"
-	keyURL       = "url"
-	keyXML       = "xml"
-	keyYAML      = "yaml"
+	keyASN1         = "asn1"
+	keyBSON         = "bson"
+	keyDatastore    = "datastore"
+	keyDefault      = "default"
+	keyJSON         = "json"
+	keyMapstructure = "mapstructure"
+	keyProtobuf     = "protobuf"
+	keyRequired     = "required"
+	keyTOML         = "toml"
+	keyURL          = "url"
+	keyValidate     = "validate"
+	keyXML          = "xml"
+	keyYAML         = "yaml"
 )
 
 func (w lintStructTagRule) checkTagNameIfNeed(tag *structtag.Tag) (string, bool) {
@@ -196,6 +199,11 @@ func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
 			if !ok {
 				w.addFailure(f.Tag, msg)
 			}
+		case keyMapstructure:
+			msg, ok := w.checkMapstructureTag(tag.Options)
+			if !ok {
+				w.addFailure(f.Tag, msg)
+			}
 		case keyProtobuf:
 			msg, ok := w.checkProtobufTag(tag)
 			if !ok {
@@ -205,11 +213,22 @@ func (w lintStructTagRule) checkTaggedField(f *ast.Field) {
 			if tag.Name != "true" && tag.Name != "false" {
 				w.addFailure(f.Tag, "required should be 'true' or 'false'")
 			}
+		case keyTOML:
+			msg, ok := w.checkTOMLTag(tag.Options)
+			if !ok {
+				w.addFailure(f.Tag, msg)
+			}
 		case keyURL:
 			msg, ok := w.checkURLTag(tag.Options)
 			if !ok {
 				w.addFailure(f.Tag, msg)
 			}
+		case keyValidate:
+			opts := append([]string{tag.Name}, tag.Options...)
+			msg, ok := w.checkValidateTag(opts)
+			if !ok {
+				w.addFailure(f.Tag, msg)
+			}
 		case keyXML:
 			msg, ok := w.checkXMLTag(tag.Options)
 			if !ok {
@@ -379,6 +398,89 @@ func (w lintStructTagRule) checkDatastoreTag(options []string) (string, bool) {
 	return "", true
 }
 
+func (w lintStructTagRule) checkMapstructureTag(options []string) (string, bool) {
+	for _, opt := range options {
+		switch opt {
+		case "omitempty", "reminder", "squash":
+		default:
+			if w.isUserDefined(keyMapstructure, opt) {
+				continue
+			}
+			return fmt.Sprintf("unknown option '%s' in Mapstructure tag", opt), false
+		}
+	}
+
+	return "", true
+}
+
+func (w lintStructTagRule) checkValidateTag(options []string) (string, bool) {
+	previousOption := ""
+	seenKeysOption := false
+	for _, opt := range options {
+		switch opt {
+		case "keys":
+			if previousOption != "dive" {
+				return "option 'keys' must follow a 'dive' option in validate tag", false
+			}
+			seenKeysOption = true
+		case "endkeys":
+			if !seenKeysOption {
+				return "option 'endkeys' without a previous 'keys' option in validate tag", false
+			}
+			seenKeysOption = false
+		default:
+			parts := strings.Split(opt, "|")
+			errMsg, ok := w.checkValidateOptionsAlternatives(parts)
+			if !ok {
+				return errMsg, false
+			}
+		}
+		previousOption = opt
+	}
+
+	return "", true
+}
+
+func (w lintStructTagRule) checkTOMLTag(options []string) (string, bool) {
+	for _, opt := range options {
+		switch opt {
+		case "omitempty":
+		default:
+			if w.isUserDefined(keyTOML, opt) {
+				continue
+			}
+			return fmt.Sprintf("unknown option '%s' in TOML tag", opt), false
+		}
+	}
+
+	return "", true
+}
+
+func (w lintStructTagRule) checkValidateOptionsAlternatives(alternatives []string) (string, bool) {
+	for _, alternative := range alternatives {
+		alternative := strings.TrimSpace(alternative)
+		parts := strings.Split(alternative, "=")
+		switch len(parts) {
+		case 1:
+			badOpt, ok := areValidateOpts(parts[0])
+			if ok || w.isUserDefined(keyValidate, badOpt) {
+				continue
+			}
+			return fmt.Sprintf("unknown option '%s' in validate tag", badOpt), false
+		case 2:
+			lhs := parts[0]
+			_, ok := validateLHS[lhs]
+			if ok || w.isUserDefined(keyValidate, lhs) {
+				continue
+			}
+			return fmt.Sprintf("unknown option '%s' in validate tag", lhs), false
+		default:
+			return fmt.Sprintf("malformed options '%s' in validate tag, not expected more than one '='", alternative), false
+		}
+	}
+	return "", true
+}
+
 func (lintStructTagRule) typeValueMatch(t ast.Expr, val string) bool {
 	tID, ok := t.(*ast.Ident)
 	if !ok {
@@ -479,3 +581,154 @@ func (w lintStructTagRule) isUserDefined(key, opt string) bool {
 	}
 	return false
 }
+
+func areValidateOpts(opts string) (string, bool) {
+	parts := strings.Split(opts, "|")
+	for _, opt := range parts {
+		_, ok := validateSingleOptions[opt]
+		if !ok {
+			return opt, false
+		}
+	}
+
+	return "", true
+}
+
+var validateSingleOptions = map[string]struct{}{
+	"alpha":                     {},
+	"alphanum":                  {},
+	"alphanumunicode":           {},
+	"alphaunicode":              {},
+	"ascii":                     {},
+	"base32":                    {},
+	"base64":                    {},
+	"base64url":                 {},
+	"bcp47_language_tag":        {},
+	"boolean":                   {},
+	"bic":                       {},
+	"btc_addr":                  {},
+	"btc_addr_bech32":           {},
+	"cidr":                      {},
+	"cidrv4":                    {},
+	"cidrv6":                    {},
+	"country_code":              {},
+	"credit_card":               {},
+	"cron":                      {},
+	"cve":                       {},
+	"datauri":                   {},
+	"dir":                       {},
+	"dirpath":                   {},
+	"dive":                      {},
+	"dns_rfc1035_label":         {},
+	"e164":                      {},
+	"email":                     {},
+	"eth_addr":                  {},
+	"file":                      {},
+	"filepath":                  {},
+	"fqdn":                      {},
+	"hexadecimal":               {},
+	"hexcolor":                  {},
+	"hostname":                  {},
+	"hostname_port":             {},
+	"hostname_rfc1123":          {},
+	"hsl":                       {},
+	"hsla":                      {},
+	"html":                      {},
+	"html_encoded":              {},
+	"image":                     {},
+	"ip":                        {},
+	"ip4_addr":                  {},
+	"ip6_addr":                  {},
+	"ip_addr":                   {},
+	"ipv4":                      {},
+	"ipv6":                      {},
+	"isbn":                      {},
+	"isbn10":                    {},
+	"isbn13":                    {},
+	"isdefault":                 {},
+	"iso3166_1_alpha2":          {},
+	"iso3166_1_alpha3":          {},
+	"iscolor":                   {},
+	"json":                      {},
+	"jwt":                       {},
+	"latitude":                  {},
+	"longitude":                 {},
+	"lowercase":                 {},
+	"luhn_checksum":             {},
+	"mac":                       {},
+	"mongodb":                   {},
+	"mongodb_connection_string": {},
+	"multibyte":                 {},
+	"nostructlevel":             {},
+	"number":                    {},
+	"numeric":                   {},
+	"omitempty":                 {},
+	"printascii":                {},
+	"required":                  {},
+	"rgb":                       {},
+	"rgba":                      {},
+	"semver":                    {},
+	"ssn":                       {},
+	"structonly":                {},
+	"tcp_addr":                  {},
+	"tcp4_addr":                 {},
+	"tcp6_addr":                 {},
+	"timezone":                  {},
+	"udp4_addr":                 {},
+	"udp6_addr":                 {},
+	"ulid":                      {},
+	"unique":                    {},
+	"unix_addr":                 {},
+	"uppercase":                 {},
+	"uri":                       {},
+	"url":                       {},
+	"url_encoded":               {},
+	"urn_rfc2141":               {},
+	"uuid":                      {},
+	"uuid3":                     {},
+	"uuid4":                     {},
+	"uuid5":                     {},
+}
+
+var validateLHS = map[string]struct{}{
+	"contains":             {},
+	"containsany":          {},
+	"containsfield":        {},
+	"containsrune":         {},
+	"datetime":             {},
+	"endsnotwith":          {},
+	"endswith":             {},
+	"eq":                   {},
+	"eqfield":              {},
+	"eqcsfield":            {},
+	"excluded_if":          {},
+	"excluded_unless":      {},
+	"excludes":             {},
+	"excludesall":          {},
+	"excludesfield":        {},
+	"excludesrune":         {},
+	"gt":                   {},
+	"gtcsfield":            {},
+	"gtecsfield":           {},
+	"len":                  {},
+	"lt":                   {},
+	"lte":                  {},
+	"ltcsfield":            {},
+	"ltecsfield":           {},
+	"max":                  {},
+	"min":                  {},
+	"ne":                   {},
+	"necsfield":            {},
+	"oneof":                {},
+	"oneofci":              {},
+	"required_if":          {},
+	"required_unless":      {},
+	"required_with":        {},
+	"required_with_all":    {},
+	"required_without":     {},
+	"required_without_all": {},
+	"spicedb":              {},
+	"startsnotwith":        {},
+	"startswith":           {},
+	"unique":               {},
+}
diff --git a/rule/unused_param.go b/rule/unused_param.go
index f57c59b..7131f5d 100644
--- a/rule/unused_param.go
+++ b/rule/unused_param.go
@@ -38,7 +38,7 @@ func (r *UnusedParamRule) Configure(args lint.Arguments) error {
 	// Arguments = [{allowRegex="^_"}]
 	allowRegexStr, ok := allowRegexParam.(string)
 	if !ok {
-		panic(fmt.Errorf("error configuring %s rule: allowRegex is not string but [%T]", r.Name(), allowRegexParam))
+		return fmt.Errorf("error configuring %s rule: allowRegex is not string but [%T]", r.Name(), allowRegexParam)
 	}
 	var err error
 	r.allowRegex, err = regexp.Compile(allowRegexStr)
diff --git a/rule/unused_receiver.go b/rule/unused_receiver.go
index 13ca39d..0713d07 100644
--- a/rule/unused_receiver.go
+++ b/rule/unused_receiver.go
@@ -36,7 +36,7 @@ func (r *UnusedReceiverRule) Configure(args lint.Arguments) error {
 	// Arguments = [{allowRegex="^_"}]
 	allowRegexStr, ok := allowRegexParam.(string)
 	if !ok {
-		panic(fmt.Errorf("error configuring [unused-receiver] rule: allowRegex is not string but [%T]", allowRegexParam))
+		return fmt.Errorf("error configuring [unused-receiver] rule: allowRegex is not string but [%T]", allowRegexParam)
 	}
 	var err error
 	r.allowRegex, err = regexp.Compile(allowRegexStr)


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 5800b39..2597f16 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -12,6 +12,7 @@ jobs:
       fail-fast: false
       matrix:
         go-version:
+          - 1.22
           - oldstable
           - stable
     steps:
diff --git a/cli/main_test.go b/cli/main_test.go
index e3dca7a..2929fa2 100644
--- a/cli/main_test.go
+++ b/cli/main_test.go
@@ -1,6 +1,7 @@
 package cli
 
 import (
+	goversion "go/version"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -96,45 +97,27 @@ func TestXDGConfigDirNoFile(t *testing.T) {
 }
 
 func TestGetVersion(t *testing.T) {
-	tests := []struct {
-		name    string
-		version string
-		commit  string
-		date    string
-		builtBy string
-		want    string
-	}{
-		// TODO re-activate the testcase
-		/*
-			{
-				name:    "Development version",
-				version: defaultVersion,
-				commit:  defaultCommit,
-				date:    defaultDate,
-				builtBy: defaultBuilder,
-				want:    "version \n",
-			},
-		*/
-		{
-			name:    "Release version",
-			version: "v1.5.0-12-g7ee4500-dev",
-			commit:  "7ee4500e125e2d1b12653b2c8e140fec380919b4",
-			date:    "2024-11-15 10:52 UTC",
-			builtBy: "builder",
-			want: `Version:	v1.5.0-12-g7ee4500-dev
+	t.Run("Development version", func(t *testing.T) {
+		got := getVersion(defaultBuilder, defaultDate, defaultCommit, defaultVersion)
+		want := "version (devel)\n"
+		if goversion.Compare(runtime.Version(), "go1.24") < 0 {
+			want = "version \n"
+		}
+
+		if got != want {
+			t.Errorf("getVersion() = %q, want %q", got, want)
+		}
+	})
+
+	t.Run("Release version", func(t *testing.T) {
+		got := getVersion("builder", "2024-11-15 10:52 UTC", "7ee4500e125e2d1b12653b2c8e140fec380919b4", "v1.5.0-12-g7ee4500-dev")
+		want := `Version:	v1.5.0-12-g7ee4500-dev
 Commit:		7ee4500e125e2d1b12653b2c8e140fec380919b4
 Built		2024-11-15 10:52 UTC by builder
-`,
-		},
-	}
+`
 
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := getVersion(tt.builtBy, tt.date, tt.commit, tt.version)
-
-			if got != tt.want {
-				t.Errorf("getVersion() = %q, want %q", got, tt.want)
-			}
-		})
-	}
+		if got != want {
+			t.Errorf("getVersion() = %q, want %q", got, want)
+		}
+	})
 }
diff --git a/lint/file_test.go b/lint/file_test.go
new file mode 100644
index 0000000..b24946f
--- /dev/null
+++ b/lint/file_test.go
@@ -0,0 +1,182 @@
+package lint
+
+import (
+	"go/ast"
+	"go/token"
+	"testing"
+)
+
+func TestFile_disabledIntervals(t *testing.T) {
+	buildCommentGroups := func(comments ...string) []*ast.CommentGroup {
+		commentGroups := make([]*ast.CommentGroup, 0, len(comments))
+		for _, c := range comments {
+			commentGroups = append(commentGroups, &ast.CommentGroup{
+				List: []*ast.Comment{
+					{Text: c},
+				},
+			})
+		}
+		return commentGroups
+	}
+
+	tests := []struct {
+		name     string
+		comments []*ast.CommentGroup
+		expected disabledIntervalsMap
+	}{
+		{
+			name:     "no directives",
+			comments: buildCommentGroups("// some comment"),
+			expected: disabledIntervalsMap{},
+		},
+		{
+			name:     "disable rule",
+			comments: buildCommentGroups("//revive:disable:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+						},
+						To: token.Position{
+							Filename: "test.go",
+							Line:     2147483647,
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "enable rule",
+			comments: buildCommentGroups("//revive:enable:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {},
+			},
+		},
+		{
+			name:     "disable and enable rule",
+			comments: buildCommentGroups("//revive:disable:rule1", "//revive:enable:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+						},
+						To: token.Position{
+							Filename: "test.go",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "disable-line rule",
+			comments: buildCommentGroups("//revive:disable-line:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+						},
+						To: token.Position{
+							Filename: "test.go",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "enable-line rule",
+			comments: buildCommentGroups("//revive:enable-line:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+						},
+						To: token.Position{
+							Filename: "test.go",
+							Line:     2147483647,
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "disable-next-line rule",
+			comments: buildCommentGroups("//revive:disable-next-line:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+							Line:     1,
+						},
+						To: token.Position{
+							Filename: "test.go",
+							Line:     1,
+						},
+					},
+				},
+			},
+		},
+		{
+			name:     "enable-next-line rule",
+			comments: buildCommentGroups("//revive:enable-next-line:rule1"),
+			expected: disabledIntervalsMap{
+				"rule1": {
+					{
+						RuleName: "rule1",
+						From: token.Position{
+							Filename: "test.go",
+							Line:     1,
+						},
+						To: token.Position{
+							Filename: "test.go",
+							Line:     2147483647,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := &File{
+				Name: "test.go",
+				Pkg: &Package{
+					fset: token.NewFileSet(),
+				},
+				AST: &ast.File{
+					Comments: tt.comments,
+				},
+			}
+			got := f.disabledIntervals(nil, false, make(chan Failure, 10))
+			if len(got) != len(tt.expected) {
+				t.Errorf("disabledIntervals() = got %v, want %v", got, tt.expected)
+			}
+			for rule, intervals := range got {
+				expectedIntervals, ok := tt.expected[rule]
+				if !ok {
+					t.Errorf("unexpected rule %q", rule)
+					continue
+				}
+				if len(intervals) != len(expectedIntervals) {
+					t.Errorf("intervals for rule %q = got %+v, want %+v", rule, intervals, expectedIntervals)
+					continue
+				}
+				for i, interval := range intervals {
+					if interval != expectedIntervals[i] {
+						t.Errorf("interval %d for rule %q = got %+v, want %+v", i, rule, interval, expectedIntervals[i])
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/rule/error_strings_test.go b/rule/error_strings_test.go
new file mode 100644
index 0000000..5362806
--- /dev/null
+++ b/rule/error_strings_test.go
@@ -0,0 +1,68 @@
+package rule_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestErrorStringsRule_Configure(t *testing.T) {
+	tests := []struct {
+		name      string
+		arguments lint.Arguments
+		wantErr   error
+	}{
+		{
+			name:      "Default configuration",
+			arguments: lint.Arguments{},
+		},
+		{
+			name:      "Valid custom functions",
+			arguments: lint.Arguments{"mypkg.MyErrorFunc", "errors.New"},
+		},
+		{
+			name:      "Argument not a string",
+			arguments: lint.Arguments{123},
+		},
+		{
+			name:      "Invalid package",
+			arguments: lint.Arguments{".MyErrorFunc"},
+			wantErr:   errors.New("found invalid custom function: .MyErrorFunc"),
+		},
+		{
+			name:      "Invalid function",
+			arguments: lint.Arguments{"errors."},
+			wantErr:   errors.New("found invalid custom function: errors."),
+		},
+		{
+			name:      "Invalid custom function",
+			arguments: lint.Arguments{"invalidFunction"},
+			wantErr:   errors.New("found invalid custom function: invalidFunction"),
+		},
+		{
+			name:      "Mixed valid and invalid custom functions",
+			arguments: lint.Arguments{"mypkg.MyErrorFunc", "invalidFunction", "invalidFunction2"},
+			wantErr:   errors.New("found invalid custom function: invalidFunction,invalidFunction2"),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var r rule.ErrorStringsRule
+
+			err := r.Configure(tt.arguments)
+
+			if tt.wantErr == nil {
+				if err != nil {
+					t.Errorf("Configure() unexpected non-nil error %q", err)
+				}
+				return
+			}
+			if err == nil || err.Error() != tt.wantErr.Error() {
+				t.Errorf("Configure() unexpected error: got %q, want %q", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/rule/string_format_test.go b/rule/string_format_test.go
new file mode 100644
index 0000000..ae904d4
--- /dev/null
+++ b/rule/string_format_test.go
@@ -0,0 +1,154 @@
+package rule_test
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestStringFormatConfigure(t *testing.T) {
+	type argumentsTest struct {
+		name      string
+		arguments lint.Arguments
+		wantErr   error
+	}
+	tests := []argumentsTest{
+		{
+			name: "Not a Slice",
+			arguments: lint.Arguments{
+				"this is not a slice"},
+			wantErr: errors.New("invalid configuration for string-format: argument is not a slice [argument 0, option 0]")},
+		{
+			name: "Missing Regex",
+			arguments: lint.Arguments{
+				[]any{
+					"method[0]"}},
+			wantErr: errors.New("invalid configuration for string-format: less than two slices found in argument, scope and regex are required [argument 0, option 0]")},
+		{
+			name: "Bad Argument Type",
+			arguments: lint.Arguments{
+				[]any{
+					1}},
+			wantErr: errors.New("invalid configuration for string-format: less than two slices found in argument, scope and regex are required [argument 0, option 0]")},
+		{
+			name: "Empty Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"",
+					"//"}},
+			wantErr: errors.New("invalid configuration for string-format: empty scope provided [argument 0, option 0]")},
+		{
+			name: "Small or Empty Regex",
+			arguments: lint.Arguments{
+				[]any{
+					"method[1].a",
+					"-"}},
+			wantErr: errors.New("invalid configuration for string-format: regex is too small (regexes should begin and end with '/') [argument 0, option 1]")},
+		{
+			name: "Bad Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"1.a",
+					"//"}},
+			wantErr: errors.New("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 0]")},
+		{
+			name: "Bad Regex",
+			arguments: lint.Arguments{
+				[]any{
+					"method[1].a",
+					"/(/"}},
+			wantErr: errors.New("failed to parse configuration for string-format: unable to compile /(/ as regexp [argument 0, option 1]")},
+		{
+			name: "Sample Config",
+			arguments: lint.Arguments{
+				[]any{
+					"core.WriteError[1].Message", "/^([^A-Z]$)/", "must not start with a capital letter"},
+				[]any{
+					"fmt.Errorf[0]", "/^|[^\\.!?]$/", "must not end in punctuation"},
+				[]any{
+					"panic", "/^[^\\n]*$/", "must not contain line breaks"}}},
+		{
+			name: "Underscores in Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"some_pkg._some_function_name[5].some_member",
+					"//"}}},
+		{
+			name: "Underscores in Multiple Scopes",
+			arguments: lint.Arguments{
+				[]any{
+					"fmt.Errorf[0],core.WriteError[1].Message",
+					"//"}}},
+		{
+			name: "', ' Delimiter",
+			arguments: lint.Arguments{
+				[]any{
+					"abc, mt.Errorf",
+					"//"}}},
+		{
+			name: "' ,' Delimiter",
+			arguments: lint.Arguments{
+				[]any{
+					"abc ,mt.Errorf",
+					"//"}}},
+		{
+			name: "',   ' Delimiter",
+			arguments: lint.Arguments{
+				[]any{
+					"abc,   mt.Errorf",
+					"//"}}},
+		{
+			name: "',   ' Delimiter",
+			arguments: lint.Arguments{
+				[]any{
+					"abc,   mt.Errorf",
+					"//"}}},
+		{
+			name: "Empty Middle Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"abc, ,mt.Errorf",
+					"//"}},
+			wantErr: errors.New("failed to parse configuration for string-format: empty scope in rule scopes: [argument 0, option 0, scope index 1]")},
+		{
+			name: "Empty First Scope",
+			arguments: lint.Arguments{
+				[]any{
+					",mt.Errorf",
+					"//"}},
+			wantErr: errors.New("failed to parse configuration for string-format: empty scope in rule scopes: [argument 0, option 0, scope index 0]")},
+		{
+			name: "Bad First Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"1.a,fmt.Errorf[0]",
+					"//"}},
+			wantErr: errors.New("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 0]")},
+		{
+			name: "Bad Second Scope",
+			arguments: lint.Arguments{
+				[]any{
+					"fmt.Errorf[0],1.a",
+					"//"}},
+			wantErr: errors.New("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 1]")}}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var r rule.StringFormatRule
+
+			err := r.Configure(tt.arguments)
+
+			if tt.wantErr == nil {
+				if err != nil {
+					t.Errorf("Configure() unexpected non-nil error %q", err)
+				}
+				return
+			}
+			if err == nil || err.Error() != tt.wantErr.Error() {
+				t.Errorf("Configure() unexpected error: got %q, want %q", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/test/error_strings_custom_functions_test.go b/test/error_strings_custom_functions_test.go
index 5cd67e4..255e06e 100644
--- a/test/error_strings_custom_functions_test.go
+++ b/test/error_strings_custom_functions_test.go
@@ -13,3 +13,10 @@ func TestErrorStringsWithCustomFunctions(t *testing.T) {
 		Arguments: args,
 	})
 }
+
+func TestErrorStringsIssue1243(t *testing.T) {
+	args := []any{"errors.Wrap"}
+	testRule(t, "error_strings_issue_1243", &rule.ErrorStringsRule{}, &lint.RuleConfig{
+		Arguments: args,
+	})
+}
diff --git a/test/string_format_test.go b/test/string_format_test.go
index f83a99b..2ef4e57 100644
--- a/test/string_format_test.go
+++ b/test/string_format_test.go
@@ -28,150 +28,6 @@ func TestStringFormat(t *testing.T) {
 				"must not start with 'ot'"}}})
 }
 
-func TestStringFormatArgumentParsing(t *testing.T) {
-	r := &rule.StringFormatRule{}
-	type argumentsTest struct {
-		name          string
-		config        lint.Arguments
-		expectedError *string
-	}
-	stringPtr := func(s string) *string {
-		return &s
-	}
-	tests := []argumentsTest{
-		{
-			name: "Not a Slice",
-			config: lint.Arguments{
-				"this is not a slice"},
-			expectedError: stringPtr("invalid configuration for string-format: argument is not a slice [argument 0, option 0]")},
-		{
-			name: "Missing Regex",
-			config: lint.Arguments{
-				[]any{
-					"method[0]"}},
-			expectedError: stringPtr("invalid configuration for string-format: less than two slices found in argument, scope and regex are required [argument 0, option 0]")},
-		{
-			name: "Bad Argument Type",
-			config: lint.Arguments{
-				[]any{
-					1}},
-			expectedError: stringPtr("invalid configuration for string-format: less than two slices found in argument, scope and regex are required [argument 0, option 0]")},
-		{
-			name: "Empty Scope",
-			config: lint.Arguments{
-				[]any{
-					"",
-					"//"}},
-			expectedError: stringPtr("invalid configuration for string-format: empty scope provided [argument 0, option 0]")},
-		{
-			name: "Small or Empty Regex",
-			config: lint.Arguments{
-				[]any{
-					"method[1].a",
-					"-"}},
-			expectedError: stringPtr("invalid configuration for string-format: regex is too small (regexes should begin and end with '/') [argument 0, option 1]")},
-		{
-			name: "Bad Scope",
-			config: lint.Arguments{
-				[]any{
-					"1.a",
-					"//"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 0]")},
-		{
-			name: "Bad Regex",
-			config: lint.Arguments{
-				[]any{
-					"method[1].a",
-					"/(/"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: unable to compile /(/ as regexp [argument 0, option 1]")},
-		{
-			name: "Sample Config",
-			config: lint.Arguments{
-				[]any{
-					"core.WriteError[1].Message", "/^([^A-Z]$)/", "must not start with a capital letter"},
-				[]any{
-					"fmt.Errorf[0]", "/^|[^\\.!?]$/", "must not end in punctuation"},
-				[]any{
-					"panic", "/^[^\\n]*$/", "must not contain line breaks"}}},
-		{
-			name: "Underscores in Scope",
-			config: lint.Arguments{
-				[]any{
-					"some_pkg._some_function_name[5].some_member",
-					"//"}}},
-		{
-			name: "Underscores in Multiple Scopes",
-			config: lint.Arguments{
-				[]any{
-					"fmt.Errorf[0],core.WriteError[1].Message",
-					"//"}}},
-		{
-			name: "', ' Delimiter",
-			config: lint.Arguments{
-				[]any{
-					"abc, mt.Errorf",
-					"//"}}},
-		{
-			name: "' ,' Delimiter",
-			config: lint.Arguments{
-				[]any{
-					"abc ,mt.Errorf",
-					"//"}}},
-		{
-			name: "',   ' Delimiter",
-			config: lint.Arguments{
-				[]any{
-					"abc,   mt.Errorf",
-					"//"}}},
-		{
-			name: "',   ' Delimiter",
-			config: lint.Arguments{
-				[]any{
-					"abc,   mt.Errorf",
-					"//"}}},
-		{
-			name: "Empty Middle Scope",
-			config: lint.Arguments{
-				[]any{
-					"abc, ,mt.Errorf",
-					"//"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: empty scope in rule scopes: [argument 0, option 0, scope index 1]")},
-		{
-			name: "Empty First Scope",
-			config: lint.Arguments{
-				[]any{
-					",mt.Errorf",
-					"//"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: empty scope in rule scopes: [argument 0, option 0, scope index 0]")},
-		{
-			name: "Bad First Scope",
-			config: lint.Arguments{
-				[]any{
-					"1.a,fmt.Errorf[0]",
-					"//"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 0]")},
-		{
-			name: "Bad Second Scope",
-			config: lint.Arguments{
-				[]any{
-					"fmt.Errorf[0],1.a",
-					"//"}},
-			expectedError: stringPtr("failed to parse configuration for string-format: unable to parse rule scope [argument 0, option 0, scope index 1]")}}
-
-	for _, a := range tests {
-		t.Run(a.name, func(t *testing.T) {
-			err := r.ParseArgumentsTest(a.config)
-			if err != nil {
-				if a.expectedError == nil || *err != *a.expectedError {
-					t.Errorf("unexpected panic message: %s", *err)
-				}
-			} else if a.expectedError != nil {
-				t.Error("error expected but not received")
-			}
-		})
-	}
-}
-
 func TestStringFormatDuplicatedStrings(t *testing.T) {
 	testRule(t, "string_format_issue_1063", &rule.StringFormatRule{}, &lint.RuleConfig{
 		Arguments: lint.Arguments{[]any{
diff --git a/test/struct_tag_test.go b/test/struct_tag_test.go
index 3c4eab1..389f0a8 100644
--- a/test/struct_tag_test.go
+++ b/test/struct_tag_test.go
@@ -18,6 +18,9 @@ func TestStructTagWithUserOptions(t *testing.T) {
 			"bson,gnu",
 			"url,myURLOption",
 			"datastore,myDatastoreOption",
+			"mapstructure,myMapstructureOption",
+			"validate,displayName",
+			"toml,unknown",
 		},
 	})
 }
diff --git a/testdata/error_strings_issue_1243.go b/testdata/error_strings_issue_1243.go
new file mode 100644
index 0000000..2e4d2c9
--- /dev/null
+++ b/testdata/error_strings_issue_1243.go
@@ -0,0 +1,11 @@
+package fixtures
+
+import (
+	"errors"
+	"fmt"
+)
+
+func issue1243() {
+	err := errors.New("An error occurred!") // MATCH /error strings should not be capitalized or end with punctuation or a newline/
+	fmt.Println(err)
+}
diff --git a/testdata/struct_tag.go b/testdata/struct_tag.go
index ec5e42e..330d631 100644
--- a/testdata/struct_tag.go
+++ b/testdata/struct_tag.go
@@ -141,3 +141,25 @@ type Fields struct {
 	Field      string `datastore:",noindex,flatten,omitempty"`
 	OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/
 }
+
+type MapStruct struct {
+	Field1     string `mapstructure:",squash,reminder,omitempty"`
+	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/
+}
+
+type ValidateUser struct {
+	Username    string `validate:"required,min=3,max=32"`
+	Email       string `validate:"required,email"`
+	Password    string `validate:"required,min=8,max=32"`
+	Biography   string `validate:"min=0,max=1000"`
+	DisplayName string `validate:"displayName,min=3,max=32"` // MATCH /unknown option 'displayName' in validate tag/
+	Complex     string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"`
+	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option 'keys' must follow a 'dive' option in validate tag/
+	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+}
+
+type TomlUser struct {
+	Username string `toml:"username,omitempty"`
+	Location string `toml:"location,unknown"` // MATCH /unknown option 'unknown' in TOML tag/
+}
diff --git a/testdata/struct_tag_user_options.go b/testdata/struct_tag_user_options.go
index bbdabc3..66585e6 100644
--- a/testdata/struct_tag_user_options.go
+++ b/testdata/struct_tag_user_options.go
@@ -24,3 +24,25 @@ type Fields struct {
 	Field      string `datastore:",noindex,flatten,omitempty,myDatastoreOption"`
 	OtherField string `datastore:",unknownOption"` // MATCH /unknown option 'unknownOption' in Datastore tag/
 }
+
+type MapStruct struct {
+	Field1     string `mapstructure:",squash,reminder,omitempty,myMapstructureOption"`
+	OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option 'unknownOption' in Mapstructure tag/
+}
+
+type ValidateUser struct {
+	Username    string `validate:"required,min=3,max=32"`
+	Email       string `validate:"required,email"`
+	Password    string `validate:"required,min=8,max=32"`
+	Biography   string `validate:"min=0,max=1000"`
+	DisplayName string `validate:"displayName,min=3,max=32"`
+	Complex     string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"`
+	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option 'keys' must follow a 'dive' option in validate tag/
+	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option 'endkeys' without a previous 'keys' option in validate tag/
+}
+
+type TomlUser struct {
+	Username string `toml:"username,omitempty"`
+	Location string `toml:"location,unknown"`
+}