mirror of
https://github.com/mgechev/revive.git
synced 2024-11-24 08:32:22 +02:00
a155d92342
* fix: support method call from structures for string-format * fix: added test for MR
312 lines
8.5 KiB
Go
312 lines
8.5 KiB
Go
package rule
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/mgechev/revive/lint"
|
|
)
|
|
|
|
// #region Revive API
|
|
|
|
// StringFormatRule lints strings and/or comments according to a set of regular expressions given as Arguments
|
|
type StringFormatRule struct{}
|
|
|
|
// Apply applies the rule to the given file.
|
|
func (*StringFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
|
|
var failures []lint.Failure
|
|
|
|
onFailure := func(failure lint.Failure) {
|
|
failures = append(failures, failure)
|
|
}
|
|
|
|
w := lintStringFormatRule{onFailure: onFailure}
|
|
w.parseArguments(arguments)
|
|
ast.Walk(w, file.AST)
|
|
|
|
return failures
|
|
}
|
|
|
|
// Name returns the rule name.
|
|
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 interface{})
|
|
// 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 := recover()
|
|
c <- err
|
|
}()
|
|
w.parseArguments(arguments)
|
|
}()
|
|
err := <-c
|
|
if err != nil {
|
|
e := fmt.Sprintf("%s", err)
|
|
return &e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// #region Internal structure
|
|
|
|
type lintStringFormatRule struct {
|
|
onFailure func(lint.Failure)
|
|
rules []stringFormatSubrule
|
|
}
|
|
|
|
type stringFormatSubrule struct {
|
|
parent *lintStringFormatRule
|
|
scope stringFormatSubruleScope
|
|
regexp *regexp.Regexp
|
|
negated bool
|
|
errorMessage string
|
|
}
|
|
|
|
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)
|
|
field string // (optional) If the argument to be checked is a struct, which member of the struct is checked against the rule (top level members only)
|
|
}
|
|
|
|
// Regex inserted to match valid function/struct field identifiers
|
|
const identRegex = "[_A-Za-z][_A-Za-z0-9]*"
|
|
|
|
var parseStringFormatScope = regexp.MustCompile(
|
|
fmt.Sprintf("^(%s(?:\\.%s)?)(?:\\[([0-9]+)\\](?:\\.(%s))?)?$", identRegex, identRegex, identRegex))
|
|
|
|
// #endregion
|
|
|
|
// #region Argument parsing
|
|
|
|
func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) {
|
|
for i, argument := range arguments {
|
|
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, negated bool, errorMessage string) {
|
|
g, ok := argument.([]interface{}) // Cast to generic slice first
|
|
if !ok {
|
|
w.configError("argument is not a slice", ruleNum, 0)
|
|
}
|
|
if len(g) < 2 {
|
|
w.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 {
|
|
w.configError("unexpected value, string was expected", ruleNum, i)
|
|
}
|
|
rule[i] = val
|
|
}
|
|
|
|
// Validate scope and regex length
|
|
if rule[0] == "" {
|
|
w.configError("empty scope provided", ruleNum, 0)
|
|
} else if len(rule[1]) < 2 {
|
|
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)
|
|
}
|
|
}
|
|
if len(matches[3]) > 0 {
|
|
scope.field = matches[3]
|
|
}
|
|
|
|
// Strip / characters from the beginning and end of rule[1] before compiling
|
|
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)
|
|
}
|
|
|
|
// Use custom error message if provided
|
|
if len(rule) == 3 {
|
|
errorMessage = rule[2]
|
|
}
|
|
return scope, regex, negated, errorMessage
|
|
}
|
|
|
|
// Report an invalid config, this is specifically the user's fault
|
|
func (lintStringFormatRule) configError(msg string, ruleNum, option int) {
|
|
panic(fmt.Sprintf("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) {
|
|
panic(fmt.Sprintf("failed to parse configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option))
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// #region Node traversal
|
|
|
|
func (w lintStringFormatRule) Visit(node ast.Node) ast.Visitor {
|
|
// First, check if node is a call expression
|
|
call, ok := node.(*ast.CallExpr)
|
|
if !ok {
|
|
return w
|
|
}
|
|
|
|
// Get the name of the call expression to check against rule scope
|
|
callName, ok := w.getCallName(call)
|
|
if !ok {
|
|
return w
|
|
}
|
|
|
|
for _, rule := range w.rules {
|
|
if rule.scope.funcName == callName {
|
|
rule.Apply(call)
|
|
}
|
|
}
|
|
|
|
return w
|
|
}
|
|
|
|
// Return the name of a call expression in the form of package.Func or Func
|
|
func (lintStringFormatRule) getCallName(call *ast.CallExpr) (callName string, ok bool) {
|
|
if ident, ok := call.Fun.(*ast.Ident); ok {
|
|
// Local function call
|
|
return ident.Name, true
|
|
}
|
|
|
|
if selector, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
// Scoped function call
|
|
scope, ok := selector.X.(*ast.Ident)
|
|
if ok {
|
|
return scope.Name + "." + selector.Sel.Name, true
|
|
}
|
|
// Scoped function call inside structure
|
|
recv, ok := selector.X.(*ast.SelectorExpr)
|
|
if ok {
|
|
return recv.Sel.Name + "." + selector.Sel.Name, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// #region Linting logic
|
|
|
|
// 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 {
|
|
return
|
|
}
|
|
for _, el := range composite.Elts {
|
|
kv, ok := el.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
key, ok := kv.Key.(*ast.Ident)
|
|
if !ok || key.Name != r.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
|
|
}
|
|
}
|
|
} 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)
|
|
}
|
|
|
|
func (r *stringFormatSubrule) lintMessage(s string, node ast.Node) {
|
|
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
|
|
}
|
|
var failure string
|
|
if len(r.errorMessage) > 0 {
|
|
failure = r.errorMessage
|
|
} else {
|
|
failure = fmt.Sprintf("string literal doesn't match user defined regex /%s/", r.regexp.String())
|
|
}
|
|
r.parent.onFailure(lint.Failure{
|
|
Confidence: 1,
|
|
Failure: failure,
|
|
Node: node,
|
|
})
|
|
}
|
|
|
|
// #endregion
|