blob: 311334c053b390503ba449ff91f1b32be96f4dba [file] [log] [blame]
Tim Windelschmidt1f4590b2025-07-29 23:05:36 +02001// Copyright 2021 The Cockroach Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package errwrap
5
6import (
7 "fmt"
8 "go/ast"
9 "go/constant"
10 "go/types"
11 "regexp"
12 "strings"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/ast/inspector"
17
18 "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/passesutil"
19)
20
21// Doc documents this pass.
22const Doc = `checks for unwrapped errors.
23
24This linter checks that:
25
26- err.Error() is not passed as an argument to an error-creating
27 function.
28
29- the '%s', '%v', and '%+v' format verbs are not used to format
30 errors when creating a new error.
31
32In both cases, an error-wrapping function can be used to correctly
33preserve the chain of errors so that user-directed hints, links to
34documentation issues, and telemetry data are all propagated.
35
36It is possible for a call site to opt the format/message string
37out of the linter using /* nolint:errwrap */ on or before the line
38that creates the error.`
39
40var errorType = types.Universe.Lookup("error").Type().String()
41
42// Analyzer checks for improperly wrapped errors.
43var Analyzer = &analysis.Analyzer{
44 Name: "errwrap",
45 Doc: Doc,
46 Requires: []*analysis.Analyzer{inspect.Analyzer},
47 Run: run,
48}
49
50func run(pass *analysis.Pass) (interface{}, error) {
51 inspctr := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
52 nodeFilter := []ast.Node{
53 (*ast.CallExpr)(nil),
54 }
55
56 inspctr.Preorder(nodeFilter, func(n ast.Node) {
57 // Catch-all for possible bugs in the linter code.
58 defer func() {
59 if r := recover(); r != nil {
60 if err, ok := r.(error); ok {
61 pass.Reportf(n.Pos(), "internal linter error: %v", err)
62 return
63 }
64 panic(r)
65 }
66 }()
67
68 callExpr, ok := n.(*ast.CallExpr)
69 if !ok {
70 return
71 }
72 if pass.TypesInfo.TypeOf(callExpr).String() != errorType {
73 return
74 }
75 sel, ok := callExpr.Fun.(*ast.SelectorExpr)
76 if !ok {
77 return
78 }
79 obj, ok := pass.TypesInfo.Uses[sel.Sel]
80 if !ok {
81 return
82 }
83 fn, ok := obj.(*types.Func)
84 if !ok {
85 return
86 }
87 pkg := obj.Pkg()
88 if pkg == nil {
89 return
90 }
91
92 // Skip files generated by go-bindata.
93 file := pass.Fset.File(n.Pos())
94 if strings.HasSuffix(file.Name(), "/embedded.go") {
95 return
96 }
97 fnName := stripVendor(fn.FullName())
98
99 // Check that none of the arguments are err.Error()
100 if _, found := ErrorFnFormatStringIndex[fnName]; found {
101 for i := range callExpr.Args {
102 if isErrorStringCall(pass, callExpr.Args[i]) {
103 // If the argument is opting out of the linter with a special
104 // comment, tolerate that.
105 if passesutil.HasNolintComment(pass, sel, "errwrap") {
106 continue
107 }
108
109 pass.Report(analysis.Diagnostic{
110 Pos: n.Pos(),
111 Message: fmt.Sprintf(
112 "err.Error() is passed to %s.%s; use pgerror.Wrap/errors.Wrap/errors.CombineErrors/"+
113 "errors.WithSecondaryError/errors.NewAssertionErrorWithWrappedErrf instead",
114 pkg.Name(), fn.Name()),
115 })
116 }
117 }
118 }
119
120 // Check that the format string does not use %s or %v for an error.
121 formatStringIdx, ok := ErrorFnFormatStringIndex[fnName]
122 if !ok || formatStringIdx < 0 {
123 // Not an error formatting function.
124 return
125 }
126
127 // Find all % fields in the format string.
128 formatVerbs, ok := getFormatStringVerbs(pass, callExpr, formatStringIdx)
129 if !ok {
130 return
131 }
132
133 // For any arguments that are errors, check whether the wrapping verb
134 // is %s or %v.
135 args := callExpr.Args[formatStringIdx+1:]
136 for i := 0; i < len(args) && i < len(formatVerbs); i++ {
137 if pass.TypesInfo.TypeOf(args[i]).String() != errorType {
138 continue
139 }
140
141 if formatVerbs[i] == "%v" || formatVerbs[i] == "%+v" || formatVerbs[i] == "%s" {
142 // If the argument is opting out of the linter with a special
143 // comment, tolerate that.
144 if passesutil.HasNolintComment(pass, sel, "errwrap") {
145 continue
146 }
147
148 pass.Report(analysis.Diagnostic{
149 Pos: n.Pos(),
150 Message: fmt.Sprintf(
151 "non-wrapped error is passed to %s.%s; use pgerror.Wrap/errors.Wrap/errors.CombineErrors/"+
152 "errors.WithSecondaryError/errors.NewAssertionErrorWithWrappedErrf instead",
153 pkg.Name(), fn.Name(),
154 ),
155 })
156 }
157 }
158 })
159
160 return nil, nil
161}
162
163// isErrorStringCall tests whether the expression is a string expression that
164// is the result of an `(error).Error()` method call.
165func isErrorStringCall(pass *analysis.Pass, expr ast.Expr) bool {
166 if call, ok := expr.(*ast.CallExpr); ok {
167 if pass.TypesInfo.TypeOf(call).String() == "string" {
168 if callSel, ok := call.Fun.(*ast.SelectorExpr); ok {
169 fun := pass.TypesInfo.Uses[callSel.Sel].(*types.Func)
170 return fun.Type().String() == "func() string" && fun.Name() == "Error"
171 }
172 }
173 }
174 return false
175}
176
177// formatVerbRegexp naively matches format string verbs. This does not take
178// modifiers such as padding into account.
179var formatVerbRegexp = regexp.MustCompile(`%([^%+]|\+v)`)
180
181// getFormatStringVerbs return an array of all `%` format verbs from the format
182// string argument of a function call.
183// Based on https://github.com/polyfloyd/go-errorlint/blob/e4f368f0ae6983eb40821ba4f88dc84ac51aef5b/errorlint/lint.go#L88
184func getFormatStringVerbs(
185 pass *analysis.Pass, call *ast.CallExpr, formatStringIdx int,
186) ([]string, bool) {
187 if len(call.Args) <= formatStringIdx {
188 return nil, false
189 }
190 strLit, ok := call.Args[formatStringIdx].(*ast.BasicLit)
191 if !ok {
192 // Ignore format strings that are not literals.
193 return nil, false
194 }
195 formatString := constant.StringVal(pass.TypesInfo.Types[strLit].Value)
196
197 return formatVerbRegexp.FindAllString(formatString, -1), true
198}
199
200func stripVendor(s string) string {
201 if i := strings.Index(s, "/vendor/"); i != -1 {
202 s = s[i+len("/vendor/"):]
203 }
204 return s
205}