1
0
mirror of https://github.com/mgechev/revive.git synced 2025-01-08 03:13:27 +02:00

Allows inversing the semantics of string-format rule configurations (#765)

* allows negating the regexp

* updates rule documentation

* adds mgechev remarks
This commit is contained in:
chavacava 2022-10-24 20:48:41 +02:00 committed by GitHub
parent 06881a9f54
commit 32a0cb8052
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 38 additions and 9 deletions

View File

@ -565,11 +565,14 @@ This is geared towards user facing applications where string literals are often
_Configuration_: Each argument is a slice containing 2-3 strings: a scope, a regex, and an optional error message. _Configuration_: Each argument is a slice containing 2-3 strings: a scope, a regex, and an optional error message.
1. The first string defines a scope. This controls which string literals the regex will apply to, and is defined as a function argument. It must contain at least a function name (`core.WriteError`). Scopes may optionally contain a number specifying which argument in the function to check (`core.WriteError[1]`), as well as a struct field (`core.WriteError[1].Message`, only works for top level fields). Function arguments are counted starting at 0, so `[0]` would refer to the first argument, `[1]` would refer to the second, etc. If no argument number is provided, the first argument will be used (same as `[0]`). 1. The first string defines a **scope**. This controls which string literals the regex will apply to, and is defined as a function argument. It must contain at least a function name (`core.WriteError`). Scopes may optionally contain a number specifying which argument in the function to check (`core.WriteError[1]`), as well as a struct field (`core.WriteError[1].Message`, only works for top level fields). Function arguments are counted starting at 0, so `[0]` would refer to the first argument, `[1]` would refer to the second, etc. If no argument number is provided, the first argument will be used (same as `[0]`).
2. The second string is a regular expression (beginning and ending with a `/` character), which will be used to check the string literals in the scope. 2. The second string is a **regular expression** (beginning and ending with a `/` character), which will be used to check the string literals in the scope. The default semantics is "_strings matching the regular expression are OK_". If you need to inverse the semantics you can add a `!` just before the first `/`. Examples:
3. The third string (optional) is a message containing the purpose for the regex, which will be used in lint errors. * with `"/^[A-Z].*$/"` the rule will **accept** strings starting with capital letters
* with `"!/^[A-Z].*$/"` the rule will a **fail** on strings starting with capital letters
3. The third string (optional) is a **message** containing the purpose for the regex, which will be used in lint errors.
Example: Example:

View File

@ -68,6 +68,7 @@ type stringFormatSubrule struct {
parent *lintStringFormatRule parent *lintStringFormatRule
scope stringFormatSubruleScope scope stringFormatSubruleScope
regexp *regexp.Regexp regexp *regexp.Regexp
negated bool
errorMessage string errorMessage string
} }
@ -89,17 +90,18 @@ var parseStringFormatScope = regexp.MustCompile(
func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) { func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) {
for i, argument := range arguments { for i, argument := range arguments {
scope, regex, errorMessage := w.parseArgument(argument, i) scope, regex, negated, errorMessage := w.parseArgument(argument, i)
w.rules = append(w.rules, stringFormatSubrule{ w.rules = append(w.rules, stringFormatSubrule{
parent: w, parent: w,
scope: scope, scope: scope,
regexp: regex, regexp: regex,
negated: negated,
errorMessage: errorMessage, errorMessage: errorMessage,
}) })
} }
} }
func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (scope stringFormatSubruleScope, regex *regexp.Regexp, errorMessage string) { func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (scope stringFormatSubruleScope, regex *regexp.Regexp, negated bool, errorMessage string) {
g, ok := argument.([]interface{}) // Cast to generic slice first g, ok := argument.([]interface{}) // Cast to generic slice first
if !ok { if !ok {
w.configError("argument is not a slice", ruleNum, 0) w.configError("argument is not a slice", ruleNum, 0)
@ -146,7 +148,12 @@ func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (
} }
// Strip / characters from the beginning and end of rule[1] before compiling // Strip / characters from the beginning and end of rule[1] before compiling
regex, err := regexp.Compile(rule[1][1 : len(rule[1])-1]) negated = rule[1][0] == '!'
offset := 1
if negated {
offset++
}
regex, err := regexp.Compile(rule[1][offset : len(rule[1])-1])
if err != nil { if err != nil {
w.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1) w.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1)
} }
@ -155,7 +162,7 @@ func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (
if len(rule) == 3 { if len(rule) == 3 {
errorMessage = rule[2] errorMessage = rule[2]
} }
return scope, regex, errorMessage return scope, regex, negated, errorMessage
} }
// Report an invalid config, this is specifically the user's fault // Report an invalid config, this is specifically the user's fault
@ -261,7 +268,26 @@ func (r *stringFormatSubrule) Apply(call *ast.CallExpr) {
} }
func (r *stringFormatSubrule) lintMessage(s string, node ast.Node) { func (r *stringFormatSubrule) lintMessage(s string, node ast.Node) {
// Fail if the string doesn't match the user's regex if r.negated {
if !r.regexp.MatchString(s) {
return
}
// Fail if the string does match the user's regex
var failure string
if len(r.errorMessage) > 0 {
failure = r.errorMessage
} else {
failure = fmt.Sprintf("string literal matches user defined regex /%s/", r.regexp.String())
}
r.parent.onFailure(lint.Failure{
Confidence: 1,
Failure: failure,
Node: node,
})
return
}
// Fail if the string does NOT match the user's regex
if r.regexp.MatchString(s) { if r.regexp.MatchString(s) {
return return
} }

View File

@ -20,7 +20,7 @@ func TestStringFormat(t *testing.T) {
"/[^\\.]$/"}, // Must not end with a period "/[^\\.]$/"}, // Must not end with a period
[]interface{}{ []interface{}{
"s.Method3[2]", "s.Method3[2]",
"/^[^Tt][^Hh]/", "!/^[Tt][Hh]/",
"must not start with 'th'"}}}) "must not start with 'th'"}}})
} }