github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/string-format.go (about) 1 package rule 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "regexp" 8 "strconv" 9 10 "github.com/songshiyun/revive/lint" 11 ) 12 13 // #region Revive API 14 15 // StringFormatRule lints strings and/or comments according to a set of regular expressions given as Arguments 16 type StringFormatRule struct{} 17 18 // Apply applies the rule to the given file. 19 func (r *StringFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure { 20 var failures []lint.Failure 21 22 onFailure := func(failure lint.Failure) { 23 failures = append(failures, failure) 24 } 25 26 w := lintStringFormatRule{onFailure: onFailure} 27 w.parseArguments(arguments) 28 ast.Walk(w, file.AST) 29 30 return failures 31 } 32 33 // Name returns the rule name. 34 func (r *StringFormatRule) Name() string { 35 return "string-format" 36 } 37 38 // 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 39 func (r *StringFormatRule) ParseArgumentsTest(arguments lint.Arguments) *string { 40 w := lintStringFormatRule{} 41 c := make(chan interface{}) 42 // Parse the arguments in a goroutine, defer a recover() call, return the error encountered (or nil if there was no error) 43 go func() { 44 defer func() { 45 err := recover() 46 c <- err 47 }() 48 w.parseArguments(arguments) 49 }() 50 err := <-c 51 if err != nil { 52 e := fmt.Sprintf("%s", err) 53 return &e 54 } 55 return nil 56 } 57 58 // #endregion 59 60 // #region Internal structure 61 62 type lintStringFormatRule struct { 63 onFailure func(lint.Failure) 64 65 rules []stringFormatSubrule 66 stringDeclarations map[string]string 67 } 68 69 type stringFormatSubrule struct { 70 parent *lintStringFormatRule 71 scope stringFormatSubruleScope 72 regexp *regexp.Regexp 73 errorMessage string 74 } 75 76 type stringFormatSubruleScope struct { 77 funcName string // Function name the rule is scoped to 78 argument int // (optional) Which argument in calls to the function is checked against the rule (the first argument is checked by default) 79 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) 80 } 81 82 // Regex inserted to match valid function/struct field identifiers 83 const identRegex = "[_A-Za-z][_A-Za-z0-9]*" 84 85 var parseStringFormatScope = regexp.MustCompile( 86 fmt.Sprintf("^(%s(?:\\.%s)?)(?:\\[([0-9]+)\\](?:\\.(%s))?)?$", identRegex, identRegex, identRegex)) 87 88 // #endregion 89 90 // #region Argument parsing 91 92 func (w *lintStringFormatRule) parseArguments(arguments lint.Arguments) { 93 for i, argument := range arguments { 94 scope, regex, errorMessage := w.parseArgument(argument, i) 95 w.rules = append(w.rules, stringFormatSubrule{ 96 parent: w, 97 scope: scope, 98 regexp: regex, 99 errorMessage: errorMessage, 100 }) 101 } 102 } 103 104 func (w lintStringFormatRule) parseArgument(argument interface{}, ruleNum int) (scope stringFormatSubruleScope, regex *regexp.Regexp, errorMessage string) { 105 g, ok := argument.([]interface{}) // Cast to generic slice first 106 if !ok { 107 w.configError("argument is not a slice", ruleNum, 0) 108 } 109 if len(g) < 2 { 110 w.configError("less than two slices found in argument, scope and regex are required", ruleNum, len(g)-1) 111 } 112 rule := make([]string, len(g)) 113 for i, obj := range g { 114 val, ok := obj.(string) 115 if !ok { 116 w.configError("unexpected value, string was expected", ruleNum, i) 117 } 118 rule[i] = val 119 } 120 121 // Validate scope and regex length 122 if rule[0] == "" { 123 w.configError("empty scope provided", ruleNum, 0) 124 } else if len(rule[1]) < 2 { 125 w.configError("regex is too small (regexes should begin and end with '/')", ruleNum, 1) 126 } 127 128 // Parse rule scope 129 scope = stringFormatSubruleScope{} 130 matches := parseStringFormatScope.FindStringSubmatch(rule[0]) 131 if matches == nil { 132 // The rule's scope didn't match the parsing regex at all, probably a configuration error 133 w.parseError("unable to parse rule scope", ruleNum, 0) 134 } else if len(matches) != 4 { 135 // The rule's scope matched the parsing regex, but an unexpected number of submatches was returned, probably a bug 136 w.parseError(fmt.Sprintf("unexpected number of submatches when parsing scope: %d, expected 4", len(matches)), ruleNum, 0) 137 } 138 scope.funcName = matches[1] 139 if len(matches[2]) > 0 { 140 var err error 141 scope.argument, err = strconv.Atoi(matches[2]) 142 if err != nil { 143 w.parseError("unable to parse argument number in rule scope", ruleNum, 0) 144 } 145 } 146 if len(matches[3]) > 0 { 147 scope.field = matches[3] 148 } 149 150 // Strip / characters from the beginning and end of rule[1] before compiling 151 regex, err := regexp.Compile(rule[1][1 : len(rule[1])-1]) 152 if err != nil { 153 w.parseError(fmt.Sprintf("unable to compile %s as regexp", rule[1]), ruleNum, 1) 154 } 155 156 // Use custom error message if provided 157 if len(rule) == 3 { 158 errorMessage = rule[2] 159 } 160 return scope, regex, errorMessage 161 } 162 163 // Report an invalid config, this is specifically the user's fault 164 func (w lintStringFormatRule) configError(msg string, ruleNum, option int) { 165 panic(fmt.Sprintf("invalid configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option)) 166 } 167 168 // Report a general config parsing failure, this may be the user's fault, but it isn't known for certain 169 func (w lintStringFormatRule) parseError(msg string, ruleNum, option int) { 170 panic(fmt.Sprintf("failed to parse configuration for string-format: %s [argument %d, option %d]", msg, ruleNum, option)) 171 } 172 173 // #endregion 174 175 // #region Node traversal 176 177 func (w lintStringFormatRule) Visit(node ast.Node) ast.Visitor { 178 // First, check if node is a call expression 179 call, ok := node.(*ast.CallExpr) 180 if !ok { 181 return w 182 } 183 184 // Get the name of the call expression to check against rule scope 185 callName, ok := w.getCallName(call) 186 if !ok { 187 return w 188 } 189 190 for _, rule := range w.rules { 191 if rule.scope.funcName == callName { 192 rule.Apply(call) 193 } 194 } 195 196 return w 197 } 198 199 // Return the name of a call expression in the form of package.Func or Func 200 func (w lintStringFormatRule) getCallName(call *ast.CallExpr) (callName string, ok bool) { 201 if ident, ok := call.Fun.(*ast.Ident); ok { 202 // Local function call 203 return ident.Name, true 204 } 205 206 if selector, ok := call.Fun.(*ast.SelectorExpr); ok { 207 // Scoped function call 208 scope, ok := selector.X.(*ast.Ident) 209 if !ok { 210 return "", false 211 } 212 return scope.Name + "." + selector.Sel.Name, true 213 } 214 215 return "", false 216 } 217 218 // #endregion 219 220 // #region Linting logic 221 222 // Apply a single format rule to a call expression (should be done after verifying the that the call expression matches the rule's scope) 223 func (rule stringFormatSubrule) Apply(call *ast.CallExpr) { 224 if len(call.Args) <= rule.scope.argument { 225 return 226 } 227 228 arg := call.Args[rule.scope.argument] 229 var lit *ast.BasicLit 230 if len(rule.scope.field) > 0 { 231 // Try finding the scope's Field, treating arg as a composite literal 232 composite, ok := arg.(*ast.CompositeLit) 233 if !ok { 234 return 235 } 236 for _, el := range composite.Elts { 237 kv, ok := el.(*ast.KeyValueExpr) 238 if !ok { 239 continue 240 } 241 key, ok := kv.Key.(*ast.Ident) 242 if !ok || key.Name != rule.scope.field { 243 continue 244 } 245 246 // 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 247 lit, ok = kv.Value.(*ast.BasicLit) 248 if !ok || lit.Kind != token.STRING { 249 return 250 } 251 } 252 } else { 253 var ok bool 254 // Treat arg as a string literal 255 lit, ok = arg.(*ast.BasicLit) 256 if !ok || lit.Kind != token.STRING { 257 return 258 } 259 } 260 // Unquote the string literal before linting 261 unquoted := lit.Value[1 : len(lit.Value)-1] 262 rule.lintMessage(unquoted, lit) 263 } 264 265 func (rule stringFormatSubrule) lintMessage(s string, node ast.Node) { 266 // Fail if the string doesn't match the user's regex 267 if rule.regexp.MatchString(s) { 268 return 269 } 270 var failure string 271 if len(rule.errorMessage) > 0 { 272 failure = rule.errorMessage 273 } else { 274 failure = fmt.Sprintf("string literal doesn't match user defined regex /%s/", rule.regexp.String()) 275 } 276 rule.parent.onFailure(lint.Failure{ 277 Confidence: 1, 278 Failure: failure, 279 Node: node, 280 }) 281 } 282 283 // #endregion