From 32a0cb8052671004cbbc9e9e43fd65a1292341f6 Mon Sep 17 00:00:00 2001 From: chavacava Date: Mon, 24 Oct 2022 20:48:41 +0200 Subject: [PATCH] Allows inversing the semantics of `string-format` rule configurations (#765) * allows negating the regexp * updates rule documentation * adds mgechev remarks --- RULES_DESCRIPTIONS.md | 9 ++++++--- rule/string-format.go | 36 +++++++++++++++++++++++++++++++----- test/string-format_test.go | 2 +- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md index 114275f..79d9a49 100644 --- a/RULES_DESCRIPTIONS.md +++ b/RULES_DESCRIPTIONS.md @@ -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: diff --git a/rule/string-format.go b/rule/string-format.go index e7841e8..0e30ebf 100644 --- a/rule/string-format.go +++ b/rule/string-format.go @@ -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 } diff --git a/test/string-format_test.go b/test/string-format_test.go index 7f23d7e..f58c10c 100644 --- a/test/string-format_test.go +++ b/test/string-format_test.go @@ -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'"}}}) }