1 package st1005
2 3 import (
4 "go/constant"
5 "strings"
6 "unicode"
7 "unicode/utf8"
8 9 "honnef.co/go/tools/analysis/code"
10 "honnef.co/go/tools/analysis/lint"
11 "honnef.co/go/tools/analysis/report"
12 "honnef.co/go/tools/go/ir"
13 "honnef.co/go/tools/go/ir/irutil"
14 "honnef.co/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.RawDocumentation{
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 }
131