github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/stylecheck/st1005/st1005.go (about) 1 package st1005 2 3 import ( 4 "go/constant" 5 "strings" 6 "unicode" 7 "unicode/utf8" 8 9 "github.com/amarpal/go-tools/analysis/code" 10 "github.com/amarpal/go-tools/analysis/lint" 11 "github.com/amarpal/go-tools/analysis/report" 12 "github.com/amarpal/go-tools/go/ir" 13 "github.com/amarpal/go-tools/go/ir/irutil" 14 "github.com/amarpal/go-tools/internal/passes/buildir" 15 16 "golang.org/x/tools/go/analysis" 17 ) 18 19 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 20 Analyzer: &analysis.Analyzer{ 21 Name: "ST1005", 22 Run: run, 23 Requires: []*analysis.Analyzer{buildir.Analyzer}, 24 }, 25 Doc: &lint.Documentation{ 26 Title: `Incorrectly formatted error string`, 27 Text: `Error strings follow a set of guidelines to ensure uniformity and good 28 composability. 29 30 Quoting Go Code Review Comments: 31 32 > Error strings should not be capitalized (unless beginning with 33 > proper nouns or acronyms) or end with punctuation, since they are 34 > usually printed following other context. That is, use 35 > \'fmt.Errorf("something bad")\' not \'fmt.Errorf("Something bad")\', so 36 > that \'log.Printf("Reading %s: %v", filename, err)\' formats without a 37 > spurious capital letter mid-message.`, 38 Since: "2019.1", 39 MergeIf: lint.MergeIfAny, 40 }, 41 }) 42 43 var Analyzer = SCAnalyzer.Analyzer 44 45 func run(pass *analysis.Pass) (interface{}, error) { 46 objNames := map[*ir.Package]map[string]bool{} 47 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg 48 objNames[irpkg] = map[string]bool{} 49 for _, m := range irpkg.Members { 50 if typ, ok := m.(*ir.Type); ok { 51 objNames[irpkg][typ.Name()] = true 52 } 53 } 54 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 55 objNames[fn.Package()][fn.Name()] = true 56 } 57 58 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 59 if code.IsInTest(pass, fn) { 60 // We don't care about malformed error messages in tests; 61 // they're usually for direct human consumption, not part 62 // of an API 63 continue 64 } 65 for _, block := range fn.Blocks { 66 instrLoop: 67 for _, ins := range block.Instrs { 68 call, ok := ins.(*ir.Call) 69 if !ok { 70 continue 71 } 72 if !irutil.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") { 73 continue 74 } 75 76 k, ok := call.Common().Args[0].(*ir.Const) 77 if !ok { 78 continue 79 } 80 81 s := constant.StringVal(k.Value) 82 if len(s) == 0 { 83 continue 84 } 85 switch s[len(s)-1] { 86 case '.', ':', '!', '\n': 87 report.Report(pass, call, "error strings should not end with punctuation or newlines") 88 } 89 idx := strings.IndexByte(s, ' ') 90 if idx == -1 { 91 // single word error message, probably not a real 92 // error but something used in tests or during 93 // debugging 94 continue 95 } 96 word := s[:idx] 97 first, n := utf8.DecodeRuneInString(word) 98 if !unicode.IsUpper(first) { 99 continue 100 } 101 for _, c := range word[n:] { 102 if unicode.IsUpper(c) || unicode.IsDigit(c) { 103 // Word is probably an initialism or multi-word function name. Digits cover elliptic curves like 104 // P384. 105 continue instrLoop 106 } 107 } 108 109 if strings.ContainsRune(word, '(') { 110 // Might be a function call 111 continue instrLoop 112 } 113 word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) }) 114 if objNames[fn.Package()][word] { 115 // Word is probably the name of a function or type in this package 116 continue 117 } 118 // First word in error starts with a capital 119 // letter, and the word doesn't contain any other 120 // capitals, making it unlikely to be an 121 // initialism or multi-word function name. 122 // 123 // It could still be a proper noun, though. 124 125 report.Report(pass, call, "error strings should not be capitalized") 126 } 127 } 128 } 129 return nil, nil 130 }