1
0
mirror of https://github.com/mgechev/revive.git synced 2024-11-30 08:57:07 +02:00
revive/rule/string-format.go
2023-09-24 08:44:02 +02:00

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 any)
// 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 any, ruleNum int) (scope stringFormatSubruleScope, 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)
}
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