github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/error-strings.go (about) 1 package rule 2 3 import ( 4 "go/ast" 5 "go/token" 6 "strconv" 7 "unicode" 8 "unicode/utf8" 9 10 "github.com/songshiyun/revive/lint" 11 ) 12 13 // ErrorStringsRule lints given else constructs. 14 type ErrorStringsRule struct{} 15 16 // Apply applies the rule to given file. 17 func (r *ErrorStringsRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { 18 var failures []lint.Failure 19 20 errorFunctions := map[string]map[string]struct{}{ 21 "fmt": { 22 "Errorf": {}, 23 }, 24 "errors": { 25 "Errorf": {}, 26 "WithMessage": {}, 27 "Wrap": {}, 28 "New": {}, 29 "WithMessagef": {}, 30 "Wrapf": {}, 31 }, 32 } 33 34 fileAst := file.AST 35 walker := lintErrorStrings{ 36 file: file, 37 fileAst: fileAst, 38 errorFunctions: errorFunctions, 39 onFailure: func(failure lint.Failure) { 40 failures = append(failures, failure) 41 }, 42 } 43 44 ast.Walk(walker, fileAst) 45 46 return failures 47 } 48 49 // Name returns the rule name. 50 func (r *ErrorStringsRule) Name() string { 51 return "error-strings" 52 } 53 54 type lintErrorStrings struct { 55 file *lint.File 56 fileAst *ast.File 57 errorFunctions map[string]map[string]struct{} 58 onFailure func(lint.Failure) 59 } 60 61 // Visit browses the AST 62 func (w lintErrorStrings) Visit(n ast.Node) ast.Visitor { 63 ce, ok := n.(*ast.CallExpr) 64 if !ok { 65 return w 66 } 67 68 if len(ce.Args) < 1 { 69 return w 70 } 71 72 // expression matches the known pkg.function 73 ok = w.match(ce) 74 if !ok { 75 return w 76 } 77 78 str, ok := w.getMessage(ce) 79 if !ok { 80 return w 81 } 82 s, _ := strconv.Unquote(str.Value) // can assume well-formed Go 83 if s == "" { 84 return w 85 } 86 clean, conf := lintErrorString(s) 87 if clean { 88 return w 89 } 90 w.onFailure(lint.Failure{ 91 Node: str, 92 Confidence: conf, 93 Category: "errors", 94 Failure: "error strings should not be capitalized or end with punctuation or a newline", 95 }) 96 return w 97 } 98 99 // match returns true if the expression corresponds to the known pkg.function 100 // i.e.: errors.Wrap 101 func (w lintErrorStrings) match(expr *ast.CallExpr) bool { 102 sel, ok := expr.Fun.(*ast.SelectorExpr) 103 if !ok { 104 return false 105 } 106 // retrieve the package 107 id, ok := sel.X.(*ast.Ident) 108 if !ok { 109 return false 110 } 111 functions, ok := w.errorFunctions[id.Name] 112 if !ok { 113 return false 114 } 115 // retrieve the function 116 _, ok = functions[sel.Sel.Name] 117 return ok 118 } 119 120 // getMessage returns the message depending on its position 121 // returns false if the cast is unsuccessful 122 func (w lintErrorStrings) getMessage(expr *ast.CallExpr) (s *ast.BasicLit, success bool) { 123 str, ok := w.checkArg(expr, 0) 124 if ok { 125 return str, true 126 } 127 if len(expr.Args) < 2 { 128 return s, false 129 } 130 str, ok = w.checkArg(expr, 1) 131 if !ok { 132 return s, false 133 } 134 return str, true 135 } 136 137 func (lintErrorStrings) checkArg(expr *ast.CallExpr, arg int) (s *ast.BasicLit, success bool) { 138 str, ok := expr.Args[arg].(*ast.BasicLit) 139 if !ok { 140 return s, false 141 } 142 if str.Kind != token.STRING { 143 return s, false 144 } 145 return str, true 146 } 147 148 func lintErrorString(s string) (isClean bool, conf float64) { 149 const basicConfidence = 0.8 150 const capConfidence = basicConfidence - 0.2 151 first, firstN := utf8.DecodeRuneInString(s) 152 last, _ := utf8.DecodeLastRuneInString(s) 153 if last == '.' || last == ':' || last == '!' || last == '\n' { 154 return false, basicConfidence 155 } 156 if unicode.IsUpper(first) { 157 // People use proper nouns and exported Go identifiers in error strings, 158 // so decrease the confidence of warnings for capitalization. 159 if len(s) <= firstN { 160 return false, capConfidence 161 } 162 // Flag strings starting with something that doesn't look like an initialism. 163 if second, _ := utf8.DecodeRuneInString(s[firstN:]); !unicode.IsUpper(second) { 164 return false, capConfidence 165 } 166 } 167 return true, 0 168 }