1
0
mirror of https://github.com/mgechev/revive.git synced 2025-01-06 03:04:06 +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.
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:

View File

@ -68,6 +68,7 @@ type stringFormatSubrule struct {
parent *lintStringFormatRule
scope stringFormatSubruleScope
regexp *regexp.Regexp
negated bool
errorMessage string
}
@ -89,17 +90,18 @@ var parseStringFormatScope = regexp.MustCompile(
func (w *lintStringFormatRule) parseArguments(arguments lint.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{
parent: w,
scope: scope,
regexp: regex,
negated: negated,
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
if !ok {
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
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 {
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 {
errorMessage = rule[2]
}
return scope, regex, errorMessage
return scope, regex, negated, errorMessage
}
// 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) {
// 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) {
return
}

View File

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