2021-04-18 18:35:30 +02:00
package rule
import (
"fmt"
"go/ast"
"go/token"
"regexp"
"strconv"
2024-08-09 21:01:17 +02:00
"strings"
2021-04-18 18:35:30 +02:00
"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.
2022-04-10 11:55:13 +02:00
func ( * StringFormatRule ) Apply ( file * lint . File , arguments lint . Arguments ) [ ] lint . Failure {
2021-04-18 18:35:30 +02:00
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
}
2021-05-03 14:22:35 +02:00
// Name returns the rule name.
2022-04-10 11:55:13 +02:00
func ( * StringFormatRule ) Name ( ) string {
2021-04-18 18:35:30 +02:00
return "string-format"
}
2021-05-03 14:22:35 +02:00
// 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
2022-04-10 11:55:13 +02:00
func ( StringFormatRule ) ParseArgumentsTest ( arguments lint . Arguments ) * string {
2021-04-18 18:35:30 +02:00
w := lintStringFormatRule { }
2023-09-24 08:44:02 +02:00
c := make ( chan any )
2021-04-18 18:35:30 +02:00
// 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 )
2022-04-10 11:55:13 +02:00
rules [ ] stringFormatSubrule
2021-04-18 18:35:30 +02:00
}
type stringFormatSubrule struct {
parent * lintStringFormatRule
2024-08-09 21:01:17 +02:00
scopes stringFormatSubruleScopes
2021-04-18 18:35:30 +02:00
regexp * regexp . Regexp
2022-10-24 20:48:41 +02:00
negated bool
2021-04-18 18:35:30 +02:00
errorMessage string
}
2024-08-09 21:01:17 +02:00
type stringFormatSubruleScopes [ ] * stringFormatSubruleScope
2021-04-18 18:35:30 +02:00
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 {
2024-08-09 21:01:17 +02:00
scopes , regex , negated , errorMessage := w . parseArgument ( argument , i )
2021-04-18 18:35:30 +02:00
w . rules = append ( w . rules , stringFormatSubrule {
parent : w ,
2024-08-09 21:01:17 +02:00
scopes : scopes ,
2021-04-18 18:35:30 +02:00
regexp : regex ,
2022-10-24 20:48:41 +02:00
negated : negated ,
2021-04-18 18:35:30 +02:00
errorMessage : errorMessage ,
} )
}
}
2024-08-09 21:01:17 +02:00
func ( w lintStringFormatRule ) parseArgument ( argument any , ruleNum int ) ( scopes stringFormatSubruleScopes , regex * regexp . Regexp , negated bool , errorMessage string ) {
2023-09-24 08:44:02 +02:00
g , ok := argument . ( [ ] any ) // Cast to generic slice first
2021-04-18 18:35:30 +02:00
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
2021-10-23 13:25:41 +02:00
if rule [ 0 ] == "" {
2021-04-18 18:35:30 +02:00
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 )
}
2024-08-09 21:01:17 +02:00
// Parse rule scopes
rawScopes := strings . Split ( rule [ 0 ] , "," )
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 )
2021-04-18 18:35:30 +02:00
}
2024-08-09 21:01:17 +02:00
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 )
2021-04-18 18:35:30 +02:00
}
// Strip / characters from the beginning and end of rule[1] before compiling
2022-10-24 20:48:41 +02:00
negated = rule [ 1 ] [ 0 ] == '!'
offset := 1
if negated {
offset ++
}
regex , err := regexp . Compile ( rule [ 1 ] [ offset : len ( rule [ 1 ] ) - 1 ] )
2021-04-18 18:35:30 +02:00
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 ]
}
2024-08-09 21:01:17 +02:00
return scopes , regex , negated , errorMessage
2021-04-18 18:35:30 +02:00
}
// Report an invalid config, this is specifically the user's fault
2022-04-10 11:55:13 +02:00
func ( lintStringFormatRule ) configError ( msg string , ruleNum , option int ) {
2021-04-18 18:35:30 +02:00
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
2022-04-10 11:55:13 +02:00
func ( lintStringFormatRule ) parseError ( msg string , ruleNum , option int ) {
2021-04-18 18:35:30 +02:00
panic ( fmt . Sprintf ( "failed to parse configuration for string-format: %s [argument %d, option %d]" , msg , ruleNum , option ) )
}
2024-08-09 21:01:17 +02:00
// 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 ) )
}
2021-04-18 18:35:30 +02:00
// #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 {
2024-08-09 21:01:17 +02:00
for _ , scope := range rule . scopes {
if scope . funcName == callName {
2024-11-04 14:19:29 +02:00
rule . apply ( call , scope )
2024-08-09 21:01:17 +02:00
}
2021-04-18 18:35:30 +02:00
}
}
return w
}
// Return the name of a call expression in the form of package.Func or Func
2022-04-10 11:55:13 +02:00
func ( lintStringFormatRule ) getCallName ( call * ast . CallExpr ) ( callName string , ok bool ) {
2021-04-18 18:35:30 +02:00
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 )
2023-07-29 11:27:07 +02:00
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
2021-04-18 18:35:30 +02:00
}
}
return "" , false
}
// #endregion
// #region Linting logic
2024-11-04 14:19:29 +02:00
// 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 , scope * stringFormatSubruleScope ) {
if len ( call . Args ) <= scope . argument {
return
}
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 {
2021-04-18 18:35:30 +02:00
return
}
2024-11-04 14:19:29 +02:00
for _ , el := range composite . Elts {
kv , ok := el . ( * ast . KeyValueExpr )
2021-04-18 18:35:30 +02:00
if ! ok {
2024-11-04 14:19:29 +02:00
continue
2021-04-18 18:35:30 +02:00
}
2024-11-04 14:19:29 +02:00
key , ok := kv . Key . ( * ast . Ident )
if ! ok || key . Name != scope . field {
continue
2021-04-18 18:35:30 +02:00
}
2024-11-04 14:19:29 +02:00
// 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 )
2021-04-18 18:35:30 +02:00
if ! ok || lit . Kind != token . STRING {
return
}
}
2024-11-04 14:19:29 +02:00
} else {
var ok bool
// Treat arg as a string literal
lit , ok = arg . ( * ast . BasicLit )
if ! ok || lit . Kind != token . STRING {
2022-10-24 20:48:41 +02:00
return
}
2024-11-04 14:19:29 +02:00
}
// Unquote the string literal before linting
unquoted := lit . Value [ 1 : len ( lit . Value ) - 1 ]
if r . stringIsOK ( unquoted ) {
2022-10-24 20:48:41 +02:00
return
}
2024-11-07 13:58:37 +02:00
r . generateFailure ( lit )
2024-11-04 14:19:29 +02:00
}
func ( r * stringFormatSubrule ) stringIsOK ( s string ) bool {
matches := r . regexp . MatchString ( s )
if r . negated {
return ! matches
2021-04-18 18:35:30 +02:00
}
2024-11-04 14:19:29 +02:00
return matches
}
2024-11-07 13:58:37 +02:00
func ( r * stringFormatSubrule ) generateFailure ( node ast . Node ) {
2021-04-18 18:35:30 +02:00
var failure string
2024-11-04 14:19:29 +02:00
switch {
case len ( r . errorMessage ) > 0 :
2022-04-10 11:55:13 +02:00
failure = r . errorMessage
2024-11-04 14:19:29 +02:00
case r . negated :
failure = fmt . Sprintf ( "string literal matches user defined regex /%s/" , r . regexp . String ( ) )
case ! r . negated :
2022-04-10 11:55:13 +02:00
failure = fmt . Sprintf ( "string literal doesn't match user defined regex /%s/" , r . regexp . String ( ) )
2021-04-18 18:35:30 +02:00
}
2024-11-04 14:19:29 +02:00
2022-04-10 11:55:13 +02:00
r . parent . onFailure ( lint . Failure {
2021-04-18 18:35:30 +02:00
Confidence : 1 ,
Failure : failure ,
2022-03-02 09:24:55 +02:00
Node : node ,
} )
2021-04-18 18:35:30 +02:00
}
// #endregion