build/analysis: copy cockroachdb code to third_party
We are currently fetching the full cockroach repository just for a hand
full of nogo passes. As the version we use is licensed under Apache 2.0,
we can copy them to third_party, allowing us to modify them and keep them
stable.
Change-Id: Ia28b181296138eef922485b6517d1e0066766715
Reviewed-on: https://review.monogon.dev/c/monogon/+/4486
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/third_party/com_github_cockroachdb_cockroach/fmtsafe/fmtsafe.go b/third_party/com_github_cockroachdb_cockroach/fmtsafe/fmtsafe.go
new file mode 100644
index 0000000..a32efc3
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/fmtsafe/fmtsafe.go
@@ -0,0 +1,294 @@
+// Copyright 2020 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package fmtsafe
+
+import (
+ "errors"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
+)
+
+// Doc documents this pass.
+var Doc = `checks that log and error functions don't leak PII.
+
+This linter checks the following:
+
+- that the format string in Infof(), Errorf() and similar calls is a
+ constant string.
+
+ This check is essential for correctness because format strings
+ are assumed to be PII-free and always safe for reporting in
+ telemetry or PII-free logs.
+
+- that the message strings in errors.New() and similar calls that
+ construct error objects is a constant string.
+
+ This check is provided to encourage hygiene: errors
+ constructed using non-constant strings are better constructed using
+ a formatting function instead, which makes the construction more
+ readable and encourage the provision of PII-free reportable details.
+
+It is possible for a call site *in a test file* to opt the format/message
+string out of the linter using /* nolint:fmtsafe */ after the format
+argument. This escape hatch is not available in non-test code.
+`
+
+// Analyzer defines this pass.
+var Analyzer = &analysis.Analyzer{
+ Name: "fmtsafe",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+ // Our analyzer just wants to see function definitions
+ // and call points.
+ nodeFilter := []ast.Node{
+ (*ast.FuncDecl)(nil),
+ (*ast.CallExpr)(nil),
+ }
+
+ // fmtOrMsgStr, if non-nil, indicates an incoming
+ // format or message string in the argument list of the
+ // current function.
+ //
+ // The pointer is set at the beginning of every function declaration
+ // for a function recognized by this linter (= any of those listed
+ // in functions.go). In non-recognized function, it is set to nil to
+ // indicate there is no known format or message string.
+ var fmtOrMsgStr *types.Var
+ var enclosingFnName string
+
+ // Now traverse the ASTs. The preorder traversal visits each
+ // function declaration node before its body, so we always get to
+ // set fmtOrMsgStr before the call points inside the body are
+ // visited.
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ // Catch-all for possible bugs in the linter code.
+ defer func() {
+ if r := recover(); r != nil {
+ if err, ok := r.(error); ok {
+ pass.Reportf(n.Pos(), "internal linter error: %v", err)
+ return
+ }
+ panic(r)
+ }
+ }()
+
+ if fd, ok := n.(*ast.FuncDecl); ok {
+ // This is the function declaration header. Obtain the formal
+ // parameter and the name of the function being defined.
+ // We use the name in subsequent error messages to provide
+ // more context, and to facilitate the definition
+ // of precise exceptions in lint_test.go.
+ enclosingFnName, fmtOrMsgStr = maybeGetConstStr(pass, fd)
+ return
+ }
+ // At a call site.
+ call := n.(*ast.CallExpr)
+ checkCallExpr(pass, enclosingFnName, call, fmtOrMsgStr)
+ })
+ return nil, nil
+}
+
+func maybeGetConstStr(
+ pass *analysis.Pass, fd *ast.FuncDecl,
+) (enclosingFnName string, res *types.Var) {
+ if fd.Body == nil {
+ // No body. Since there won't be any callee, take
+ // an early return.
+ return "", nil
+ }
+
+ // What's the function being defined?
+ fn := pass.TypesInfo.Defs[fd.Name].(*types.Func)
+ if fn == nil {
+ return "", nil
+ }
+ fnName := stripVendor(fn.FullName())
+
+ var wantVariadic bool
+ var argIdx int
+
+ if requireConstFmt[fnName] {
+ // Expect a variadic function and the format parameter
+ // next-to-last in the parameter list.
+ wantVariadic = true
+ argIdx = -2
+ } else if requireConstMsg[fnName] {
+ // Expect a non-variadic function and the format parameter last in
+ // the parameter list.
+ wantVariadic = false
+ argIdx = -1
+ } else {
+ // Not a recognized function. Bail.
+ return fn.Name(), nil
+ }
+
+ sig := fn.Type().(*types.Signature)
+ if sig.Variadic() != wantVariadic {
+ panic(fmt.Errorf("expected variadic %v, got %v", wantVariadic, sig.Variadic()))
+ }
+
+ params := sig.Params()
+ nparams := params.Len()
+
+ // Is message or format param a string?
+ if nparams+argIdx < 0 {
+ panic(errors.New("not enough arguments"))
+ }
+ if p := params.At(nparams + argIdx); p.Type() == types.Typ[types.String] {
+ // Found it!
+ return fn.Name(), p
+ }
+ return fn.Name(), nil
+}
+
+func checkCallExpr(pass *analysis.Pass, enclosingFnName string, call *ast.CallExpr, fv *types.Var) {
+ // What's the function being called?
+ cfn := typeutil.Callee(pass.TypesInfo, call)
+ if cfn == nil {
+ // Not a call to a statically identified function.
+ // We can't lint.
+ return
+ }
+ fn, ok := cfn.(*types.Func)
+ if !ok {
+ // Not a function with a name. We can't lint either.
+ return
+ }
+
+ // What's the full name of the callee? This includes the package
+ // path and, for methods, the type of the supporting object.
+ fnName := stripVendor(fn.FullName())
+
+ var wantVariadic bool
+ var argIdx int
+ var argType string
+
+ // Do the analysis of the callee.
+ if requireConstFmt[fnName] {
+ // Expect a variadic function and the format parameter
+ // next-to-last in the parameter list.
+ wantVariadic = true
+ argIdx = -2
+ argType = "format"
+ } else if requireConstMsg[fnName] {
+ // Expect a non-variadic function and the format parameter last in
+ // the parameter list.
+ wantVariadic = false
+ argIdx = -1
+ argType = "message"
+ } else {
+ // Not a recognized function. Bail.
+ return
+ }
+
+ typ := pass.TypesInfo.Types[call.Fun].Type
+ if typ == nil {
+ panic(errors.New("can't find function type"))
+ }
+
+ sig, ok := typ.(*types.Signature)
+ if !ok {
+ panic(errors.New("can't derive signature"))
+ }
+ if sig.Variadic() != wantVariadic {
+ panic(fmt.Errorf("expected variadic %v, got %v", wantVariadic, sig.Variadic()))
+ }
+
+ idx := sig.Params().Len() + argIdx
+ if idx < 0 {
+ panic(errors.New("not enough parameters"))
+ }
+
+ lit := pass.TypesInfo.Types[call.Args[idx]].Value
+ if lit != nil {
+ // A literal or constant! All is well.
+ return
+ }
+
+ // Not a constant. If it's a variable and the variable
+ // refers to the incoming format/message from the arg list,
+ // tolerate that.
+ if fv != nil {
+ if id, ok := call.Args[idx].(*ast.Ident); ok {
+ if pass.TypesInfo.ObjectOf(id) == fv {
+ // Same arg as incoming. All good.
+ return
+ }
+ }
+ }
+
+ // If the argument is opting out of the linter with a special
+ // comment, tolerate that.
+ if hasNoLintComment(pass, call, idx) {
+ return
+ }
+
+ pass.Reportf(call.Lparen, escNl("%s(): %s argument is not a constant expression"+Tip),
+ enclosingFnName, argType)
+}
+
+// Tip is exported for use in tests.
+var Tip = `
+Tip: use YourFuncf("descriptive prefix %%s", ...) or list new formatting wrappers in pkg/testutils/lint/passes/fmtsafe/functions.go.`
+
+func hasNoLintComment(pass *analysis.Pass, call *ast.CallExpr, idx int) bool {
+ fPos, f := findContainingFile(pass, call)
+
+ if !strings.HasSuffix(fPos.Name(), "_test.go") {
+ // The nolint: escape hatch is only supported in test files.
+ return false
+ }
+
+ startPos := call.Args[idx].End()
+ endPos := call.Rparen
+ if idx < len(call.Args)-1 {
+ endPos = call.Args[idx+1].Pos()
+ }
+ for _, cg := range f.Comments {
+ if cg.Pos() > endPos || cg.End() < startPos {
+ continue
+ }
+ for _, c := range cg.List {
+ if strings.Contains(c.Text, "nolint:fmtsafe") {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func findContainingFile(pass *analysis.Pass, n ast.Node) (*token.File, *ast.File) {
+ fPos := pass.Fset.File(n.Pos())
+ for _, f := range pass.Files {
+ if pass.Fset.File(f.Pos()) == fPos {
+ return fPos, f
+ }
+ }
+ panic(fmt.Errorf("cannot file file for %v", n))
+}
+
+func stripVendor(s string) string {
+ if i := strings.Index(s, "/vendor/"); i != -1 {
+ s = s[i+len("/vendor/"):]
+ }
+ return s
+}
+
+func escNl(msg string) string {
+ return strings.ReplaceAll(msg, "\n", "\\n++")
+}