-
Notifications
You must be signed in to change notification settings - Fork 281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add multiple scopes support to string-format rule #1009
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"go/token" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/mgechev/revive/lint" | ||
) | ||
|
@@ -66,12 +67,14 @@ type lintStringFormatRule struct { | |
|
||
type stringFormatSubrule struct { | ||
parent *lintStringFormatRule | ||
scope stringFormatSubruleScope | ||
scopes stringFormatSubruleScopes | ||
regexp *regexp.Regexp | ||
negated bool | ||
errorMessage string | ||
} | ||
|
||
type stringFormatSubruleScopes []*stringFormatSubruleScope | ||
|
||
type stringFormatSubruleScope struct { | ||
funcName string // Function name the rule is scoped to | ||
argument int // (optional) Which argument in calls to the function is checked against the rule (the first argument is checked by default) | ||
|
@@ -90,18 +93,18 @@ var parseStringFormatScope = regexp.MustCompile( | |
|
||
func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) { | ||
for i, argument := range arguments { | ||
scope, regex, negated, errorMessage := w.parseArgument(argument, i) | ||
scopes, regex, negated, errorMessage := w.parseArgument(argument, i) | ||
w.rules = append(w.rules, stringFormatSubrule{ | ||
parent: w, | ||
scope: scope, | ||
scopes: scopes, | ||
regexp: regex, | ||
negated: negated, | ||
errorMessage: errorMessage, | ||
}) | ||
} | ||
} | ||
|
||
func (w lintStringFormatRule) parseArgument(argument any, ruleNum int) (scope stringFormatSubruleScope, regex *regexp.Regexp, negated bool, errorMessage string) { | ||
func (w lintStringFormatRule) parseArgument(argument any, ruleNum int) (scopes stringFormatSubruleScopes, regex *regexp.Regexp, negated bool, errorMessage string) { | ||
g, ok := argument.([]any) // Cast to generic slice first | ||
if !ok { | ||
w.configError("argument is not a slice", ruleNum, 0) | ||
|
@@ -125,26 +128,39 @@ func (w lintStringFormatRule) parseArgument(argument any, ruleNum int) (scope st | |
w.configError("regex is too small (regexes should begin and end with '/')", ruleNum, 1) | ||
} | ||
|
||
// Parse rule scope | ||
scope = stringFormatSubruleScope{} | ||
matches := parseStringFormatScope.FindStringSubmatch(rule[0]) | ||
if matches == nil { | ||
// The rule's scope didn't match the parsing regex at all, probably a configuration error | ||
w.parseError("unable to parse rule scope", ruleNum, 0) | ||
} else if len(matches) != 4 { | ||
// The rule's scope matched the parsing regex, but an unexpected number of submatches was returned, probably a bug | ||
w.parseError(fmt.Sprintf("unexpected number of submatches when parsing scope: %d, expected 4", len(matches)), ruleNum, 0) | ||
} | ||
scope.funcName = matches[1] | ||
if len(matches[2]) > 0 { | ||
var err error | ||
scope.argument, err = strconv.Atoi(matches[2]) | ||
if err != nil { | ||
w.parseError("unable to parse argument number in rule scope", ruleNum, 0) | ||
// Parse rule scopes | ||
rawScopes := strings.Split(rule[0], ",") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit worried about the fact people may use So maybe, you could use strings.TrimSpace on the rawScope in the for loop Also, xhecking that the rawScope is not empty could avoid surprises and bugs I'm pretty sure someone would face a
No matter the implementation choice to either clean them, or forbid them. It should be covered by a test |
||
|
||
scopes = make([]*stringFormatSubruleScope, 0, len(rawScopes)) | ||
for scopeNum, rawScope := range rawScopes { | ||
rawScope = strings.TrimSpace(rawScope) | ||
|
||
if len(rawScope) == 0 { | ||
w.parseScopeError("empty scope in rule scopes:", ruleNum, 0, scopeNum) | ||
} | ||
} | ||
if len(matches[3]) > 0 { | ||
scope.field = matches[3] | ||
|
||
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 | ||
w.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 | ||
w.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 { | ||
w.parseScopeError("unable to parse argument number in rule scope", ruleNum, 0, scopeNum) | ||
} | ||
} | ||
if len(matches[3]) > 0 { | ||
scope.field = matches[3] | ||
} | ||
|
||
scopes = append(scopes, &scope) | ||
} | ||
|
||
// Strip / characters from the beginning and end of rule[1] before compiling | ||
|
@@ -162,7 +178,7 @@ func (w lintStringFormatRule) parseArgument(argument any, ruleNum int) (scope st | |
if len(rule) == 3 { | ||
errorMessage = rule[2] | ||
} | ||
return scope, regex, negated, errorMessage | ||
return scopes, regex, negated, errorMessage | ||
} | ||
|
||
// Report an invalid config, this is specifically the user's fault | ||
|
@@ -175,6 +191,11 @@ func (lintStringFormatRule) parseError(msg string, ruleNum, option int) { | |
panic(fmt.Sprintf("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) { | ||
panic(fmt.Sprintf("failed to parse configuration for string-format: %s [argument %d, option %d, scope index %d]", msg, ruleNum, option, scopeNum)) | ||
} | ||
|
||
// #endregion | ||
|
||
// #region Node traversal | ||
|
@@ -193,8 +214,10 @@ func (w lintStringFormatRule) Visit(node ast.Node) ast.Visitor { | |
} | ||
|
||
for _, rule := range w.rules { | ||
if rule.scope.funcName == callName { | ||
rule.Apply(call) | ||
for _, scope := range rule.scopes { | ||
if scope.funcName == callName { | ||
rule.Apply(call) | ||
} | ||
} | ||
} | ||
|
||
|
@@ -230,45 +253,47 @@ func (lintStringFormatRule) getCallName(call *ast.CallExpr) (callName string, ok | |
|
||
// Apply a single format rule to a call expression (should be done after verifying the that the call expression matches the rule's scope) | ||
func (r *stringFormatSubrule) Apply(call *ast.CallExpr) { | ||
if len(call.Args) <= r.scope.argument { | ||
return | ||
} | ||
|
||
arg := call.Args[r.scope.argument] | ||
var lit *ast.BasicLit | ||
if len(r.scope.field) > 0 { | ||
// Try finding the scope's Field, treating arg as a composite literal | ||
composite, ok := arg.(*ast.CompositeLit) | ||
if !ok { | ||
for _, scope := range r.scopes { | ||
if len(call.Args) <= scope.argument { | ||
return | ||
} | ||
for _, el := range composite.Elts { | ||
kv, ok := el.(*ast.KeyValueExpr) | ||
|
||
arg := call.Args[scope.argument] | ||
var lit *ast.BasicLit | ||
if len(scope.field) > 0 { | ||
// Try finding the scope's Field, treating arg as a composite literal | ||
composite, ok := arg.(*ast.CompositeLit) | ||
if !ok { | ||
continue | ||
return | ||
} | ||
key, ok := kv.Key.(*ast.Ident) | ||
if !ok || key.Name != r.scope.field { | ||
continue | ||
for _, el := range composite.Elts { | ||
kv, ok := el.(*ast.KeyValueExpr) | ||
if !ok { | ||
continue | ||
} | ||
key, ok := kv.Key.(*ast.Ident) | ||
if !ok || key.Name != scope.field { | ||
continue | ||
} | ||
|
||
// We're now dealing with the exact field in the rule's scope, so if anything fails, we can safely return instead of continuing the loop | ||
lit, ok = kv.Value.(*ast.BasicLit) | ||
if !ok || lit.Kind != token.STRING { | ||
return | ||
} | ||
} | ||
|
||
// We're now dealing with the exact field in the rule's scope, so if anything fails, we can safely return instead of continuing the loop | ||
lit, ok = kv.Value.(*ast.BasicLit) | ||
} else { | ||
var ok bool | ||
// Treat arg as a string literal | ||
lit, ok = arg.(*ast.BasicLit) | ||
if !ok || lit.Kind != token.STRING { | ||
return | ||
} | ||
} | ||
} else { | ||
var ok bool | ||
// Treat arg as a string literal | ||
lit, ok = arg.(*ast.BasicLit) | ||
if !ok || lit.Kind != token.STRING { | ||
return | ||
} | ||
// Unquote the string literal before linting | ||
unquoted := lit.Value[1 : len(lit.Value)-1] | ||
r.lintMessage(unquoted, lit) | ||
} | ||
// Unquote the string literal before linting | ||
unquoted := lit.Value[1 : len(lit.Value)-1] | ||
r.lintMessage(unquoted, lit) | ||
} | ||
|
||
func (r *stringFormatSubrule) lintMessage(s string, node ast.Node) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out of curiosity: why is that a slice of pointers? (not a slice of values)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw one video on youtube where this two approaches were compared and slice of pointers use less memory little bit)