github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/testutils/lint/passes/fmtsafe/fmtsafe.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package fmtsafe 12 13 import ( 14 "fmt" 15 "go/ast" 16 "go/token" 17 "go/types" 18 "strings" 19 20 "github.com/cockroachdb/errors" 21 "golang.org/x/tools/go/analysis" 22 "golang.org/x/tools/go/analysis/passes/inspect" 23 "golang.org/x/tools/go/ast/inspector" 24 "golang.org/x/tools/go/types/typeutil" 25 ) 26 27 // Doc documents this pass. 28 var Doc = `checks that log and error functions don't leak PII. 29 30 This linter checks the following: 31 32 - that the format string in Infof(), Errorf() and similar calls is a 33 constant string. 34 35 This check is essential for correctness because format strings 36 are assumed to be PII-free and always safe for reporting in 37 telemetry or PII-free logs. 38 39 - that the message strings in errors.New() and similar calls that 40 construct error objects is a constant string. 41 42 This check is provided to encourage hygiene: errors 43 constructed using non-constant strings are better constructed using 44 a formatting function instead, which makes the construction more 45 readable and encourage the provision of PII-free reportable details. 46 47 It is possible for a call site *in a test file* to opt the format/message 48 string out of the linter using /* nolint:fmtsafe */ after the format 49 argument. This escape hatch is not available in non-test code. 50 ` 51 52 // Analyzer defines this pass. 53 var Analyzer = &analysis.Analyzer{ 54 Name: "fmtsafe", 55 Doc: Doc, 56 Requires: []*analysis.Analyzer{inspect.Analyzer}, 57 Run: run, 58 } 59 60 func run(pass *analysis.Pass) (interface{}, error) { 61 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 62 63 // Our analyzer just wants to see function definitions 64 // and call points. 65 nodeFilter := []ast.Node{ 66 (*ast.FuncDecl)(nil), 67 (*ast.CallExpr)(nil), 68 } 69 70 // fmtOrMsgStr, if non-nil, indicates an incoming 71 // format or message string in the argument list of the 72 // current function. 73 // 74 // The pointer is set at the beginning of every function declaration 75 // for a function recognized by this linter (= any of those listed 76 // in functions.go). In non-recognized function, it is set to nil to 77 // indicate there is no known format or message string. 78 var fmtOrMsgStr *types.Var 79 var enclosingFnName string 80 81 // Now traverse the ASTs. The preorder traversal visits each 82 // function declaration node before its body, so we always get to 83 // set fmtOrMsgStr before the call points inside the body are 84 // visited. 85 inspect.Preorder(nodeFilter, func(n ast.Node) { 86 // Catch-all for possible bugs in the linter code. 87 defer func() { 88 if r := recover(); r != nil { 89 if err, ok := r.(error); ok { 90 pass.Reportf(n.Pos(), "internal linter error: %v", err) 91 return 92 } 93 panic(r) 94 } 95 }() 96 97 if fd, ok := n.(*ast.FuncDecl); ok { 98 // This is the function declaration header. Obtain the formal 99 // parameter and the name of the function being defined. 100 // We use the name in subsequent error messages to provide 101 // more context, and to facilitate the definition 102 // of precise exceptions in lint_test.go. 103 enclosingFnName, fmtOrMsgStr = maybeGetConstStr(pass, fd) 104 return 105 } 106 // At a call site. 107 call := n.(*ast.CallExpr) 108 checkCallExpr(pass, enclosingFnName, call, fmtOrMsgStr) 109 }) 110 return nil, nil 111 } 112 113 func maybeGetConstStr( 114 pass *analysis.Pass, fd *ast.FuncDecl, 115 ) (enclosingFnName string, res *types.Var) { 116 if fd.Body == nil { 117 // No body. Since there won't be any callee, take 118 // an early return. 119 return "", nil 120 } 121 122 // What's the function being defined? 123 fn := pass.TypesInfo.Defs[fd.Name].(*types.Func) 124 if fn == nil { 125 return "", nil 126 } 127 fnName := stripVendor(fn.FullName()) 128 129 var wantVariadic bool 130 var argIdx int 131 132 if requireConstFmt[fnName] { 133 // Expect a variadic function and the format parameter 134 // next-to-last in the parameter list. 135 wantVariadic = true 136 argIdx = -2 137 } else if requireConstMsg[fnName] { 138 // Expect a non-variadic function and the format parameter last in 139 // the parameter list. 140 wantVariadic = false 141 argIdx = -1 142 } else { 143 // Not a recognized function. Bail. 144 return fn.Name(), nil 145 } 146 147 sig := fn.Type().(*types.Signature) 148 if sig.Variadic() != wantVariadic { 149 panic(errors.Newf("expected variadic %v, got %v", wantVariadic, sig.Variadic())) 150 } 151 152 params := sig.Params() 153 nparams := params.Len() 154 155 // Is message or format param a string? 156 if nparams+argIdx < 0 { 157 panic(errors.New("not enough arguments")) 158 } 159 if p := params.At(nparams + argIdx); p.Type() == types.Typ[types.String] { 160 // Found it! 161 return fn.Name(), p 162 } 163 return fn.Name(), nil 164 } 165 166 func checkCallExpr(pass *analysis.Pass, enclosingFnName string, call *ast.CallExpr, fv *types.Var) { 167 // What's the function being called? 168 cfn := typeutil.Callee(pass.TypesInfo, call) 169 if cfn == nil { 170 // Not a call to a statically identified function. 171 // We can't lint. 172 return 173 } 174 fn, ok := cfn.(*types.Func) 175 if !ok { 176 // Not a function with a name. We can't lint either. 177 return 178 } 179 180 // What's the full name of the callee? This includes the package 181 // path and, for methods, the type of the supporting object. 182 fnName := stripVendor(fn.FullName()) 183 184 var wantVariadic bool 185 var argIdx int 186 var argType string 187 188 // Do the analysis of the callee. 189 if requireConstFmt[fnName] { 190 // Expect a variadic function and the format parameter 191 // next-to-last in the parameter list. 192 wantVariadic = true 193 argIdx = -2 194 argType = "format" 195 } else if requireConstMsg[fnName] { 196 // Expect a non-variadic function and the format parameter last in 197 // the parameter list. 198 wantVariadic = false 199 argIdx = -1 200 argType = "message" 201 } else { 202 // Not a recognized function. Bail. 203 return 204 } 205 206 typ := pass.TypesInfo.Types[call.Fun].Type 207 if typ == nil { 208 panic(errors.New("can't find function type")) 209 } 210 211 sig, ok := typ.(*types.Signature) 212 if !ok { 213 panic(errors.New("can't derive signature")) 214 } 215 if sig.Variadic() != wantVariadic { 216 panic(errors.Newf("expected variadic %v, got %v", wantVariadic, sig.Variadic())) 217 } 218 219 idx := sig.Params().Len() + argIdx 220 if idx < 0 { 221 panic(errors.New("not enough parameters")) 222 } 223 224 lit := pass.TypesInfo.Types[call.Args[idx]].Value 225 if lit != nil { 226 // A literal constant! All is well. 227 return 228 } 229 230 // Not a constant. If it's a variable and the variable 231 // refers to the incoming format/message from the arg list, 232 // tolerate that. 233 if fv != nil { 234 if id, ok := call.Args[idx].(*ast.Ident); ok { 235 if pass.TypesInfo.ObjectOf(id) == fv { 236 // Same arg as incoming. All good. 237 return 238 } 239 } 240 } 241 242 // If the argument is opting out of the linter with a special 243 // comment, tolerate that. 244 if hasNoLintComment(pass, call, idx) { 245 return 246 } 247 248 pass.Reportf(call.Lparen, escNl("%s(): %s argument is not a constant expression"+Tip), 249 enclosingFnName, argType) 250 } 251 252 // Tip is exported for use in tests. 253 var Tip = ` 254 Tip: use YourFuncf("descriptive prefix %s", ...) or list new formatting wrappers in pkg/testutils/lint/passes/fmtsafe/functions.go.` 255 256 func hasNoLintComment(pass *analysis.Pass, call *ast.CallExpr, idx int) bool { 257 fPos, f := findContainingFile(pass, call) 258 259 if !strings.HasSuffix(fPos.Name(), "_test.go") { 260 // The nolint: escape hatch is only supported in test files. 261 return false 262 } 263 264 startPos := call.Args[idx].End() 265 endPos := call.Rparen 266 if idx < len(call.Args)-1 { 267 endPos = call.Args[idx+1].Pos() 268 } 269 for _, cg := range f.Comments { 270 if cg.Pos() > endPos || cg.End() < startPos { 271 continue 272 } 273 for _, c := range cg.List { 274 if strings.Contains(c.Text, "nolint:fmtsafe") { 275 return true 276 } 277 } 278 } 279 return false 280 } 281 282 func findContainingFile(pass *analysis.Pass, n ast.Node) (*token.File, *ast.File) { 283 fPos := pass.Fset.File(n.Pos()) 284 for _, f := range pass.Files { 285 if pass.Fset.File(f.Pos()) == fPos { 286 return fPos, f 287 } 288 } 289 panic(fmt.Errorf("cannot file file for %v", n)) 290 } 291 292 func stripVendor(s string) string { 293 if i := strings.Index(s, "/vendor/"); i != -1 { 294 s = s[i+len("/vendor/"):] 295 } 296 return s 297 } 298 299 func escNl(msg string) string { 300 return strings.ReplaceAll(msg, "\n", "\\n++") 301 }