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/build/analysis/BUILD.bazel b/build/analysis/BUILD.bazel
index 98a1563..aeabda7 100644
--- a/build/analysis/BUILD.bazel
+++ b/build/analysis/BUILD.bazel
@@ -62,12 +62,12 @@
# Append some passes provided by CockroachDB.
NOGO_PASSES += [
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/errcmp",
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/errwrap",
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/hash",
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/nilness",
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/returnerrcheck",
- "@com_github_cockroachdb_cockroach//pkg/testutils/lint/passes/unconvert",
+ "//third_party/com_github_cockroachdb_cockroach/errcmp",
+ "//third_party/com_github_cockroachdb_cockroach/errwrap",
+ "//third_party/com_github_cockroachdb_cockroach/hash",
+ "//third_party/com_github_cockroachdb_cockroach/nilness",
+ "//third_party/com_github_cockroachdb_cockroach/returnerrcheck",
+ "//third_party/com_github_cockroachdb_cockroach/unconvert",
]
# Combine all staticcheck analyzers with a list
@@ -147,6 +147,11 @@
"sqlite3.*go": "third_party",
},
},
+ "haslicense": {
+ "exclude_files": {
+ "third_party/": "third_party",
+ },
+ },
},
]
diff --git a/third_party/com_github_cockroachdb_cockroach/README.md b/third_party/com_github_cockroachdb_cockroach/README.md
new file mode 100644
index 0000000..4277879
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/README.md
@@ -0,0 +1,5 @@
+# Cockroach DB - Linter Passes
+
+This directory contains some linter passes we use in our nogo execution.
+Everything is based on v22.1.6, with a BSL change date of 2025-04-01
+and is therefore license under Apache 2.0.
\ No newline at end of file
diff --git a/third_party/com_github_cockroachdb_cockroach/errcmp/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/errcmp/BUILD.bazel
new file mode 100644
index 0000000..bd8063a
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/errcmp/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "errcmp",
+ srcs = ["errcmp.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/errcmp",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ "@org_golang_x_tools//go/ast/inspector",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/errcmp/errcmp.go b/third_party/com_github_cockroachdb_cockroach/errcmp/errcmp.go
new file mode 100644
index 0000000..0b5afb9
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/errcmp/errcmp.go
@@ -0,0 +1,131 @@
+// Copyright 2020 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package errcmp defines an Analyzer which checks
+// for usage of errors.Is instead of direct ==/!= comparisons.
+package errcmp
+
+import (
+ "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"
+)
+
+// Doc documents this pass.
+const Doc = `check for comparison of error objects`
+
+var errorType = types.Universe.Lookup("error").Type()
+
+// Analyzer checks for usage of errors.Is instead of direct ==/!=
+// comparisons.
+var Analyzer = &analysis.Analyzer{
+ Name: "errcmp",
+ 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 comparisons and casts.
+ nodeFilter := []ast.Node{
+ (*ast.BinaryExpr)(nil),
+ (*ast.TypeAssertExpr)(nil),
+ (*ast.SwitchStmt)(nil),
+ }
+
+ // Now traverse the ASTs.
+ 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 cmp, ok := n.(*ast.BinaryExpr); ok {
+ checkErrCmp(pass, cmp)
+ return
+ }
+ if cmp, ok := n.(*ast.TypeAssertExpr); ok {
+ checkErrCast(pass, cmp)
+ return
+ }
+ if cmp, ok := n.(*ast.SwitchStmt); ok {
+ checkErrSwitch(pass, cmp)
+ return
+ }
+ })
+
+ return nil, nil
+}
+
+func checkErrSwitch(pass *analysis.Pass, s *ast.SwitchStmt) {
+ if pass.TypesInfo.Types[s.Tag].Type == errorType {
+ pass.Reportf(s.Switch, escNl(`invalid direct comparison of error object
+Tip:
+ switch err { case errRef:...
+-> switch { case errors.Is(err, errRef): ...
+`))
+ }
+}
+
+func checkErrCast(pass *analysis.Pass, texpr *ast.TypeAssertExpr) {
+ if pass.TypesInfo.Types[texpr.X].Type == errorType {
+ pass.Reportf(texpr.Lparen, escNl(`invalid direct cast on error object
+Alternatives:
+ if _, ok := err.(*T); ok -> if errors.HasType(err, (*T)(nil))
+ if _, ok := err.(I); ok -> if errors.HasInterface(err, (*I)(nil))
+ if myErr, ok := err.(*T); ok -> if myErr := (*T)(nil); errors.As(err, &myErr)
+ if myErr, ok := err.(I); ok -> if myErr := (I)(nil); errors.As(err, &myErr)
+ switch err.(type) { case *T:... -> switch { case errors.HasType(err, (*T)(nil)): ...
+`))
+ }
+}
+
+func isEOFError(e ast.Expr) bool {
+ if s, ok := e.(*ast.SelectorExpr); ok {
+ if io, ok := s.X.(*ast.Ident); ok && io.Name == "io" && io.Obj == (*ast.Object)(nil) {
+ if s.Sel.Name == "EOF" || s.Sel.Name == "ErrUnexpectedEOF" {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func checkErrCmp(pass *analysis.Pass, binaryExpr *ast.BinaryExpr) {
+ switch binaryExpr.Op {
+ case token.NEQ, token.EQL:
+ if pass.TypesInfo.Types[binaryExpr.X].Type == errorType &&
+ !pass.TypesInfo.Types[binaryExpr.Y].IsNil() {
+ // We have a special case: when the RHS is io.EOF or io.ErrUnexpectedEOF.
+ // They are nearly always used with APIs that return
+ // an undecorated error.
+ if isEOFError(binaryExpr.Y) {
+ return
+ }
+
+ pass.Reportf(binaryExpr.OpPos, escNl(`use errors.Is instead of a direct comparison
+For example:
+ if errors.Is(err, errMyOwnErrReference) {
+ ...
+ }
+`))
+ }
+ }
+}
+
+func escNl(msg string) string {
+ return strings.ReplaceAll(msg, "\n", "\\n++")
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/errwrap/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/errwrap/BUILD.bazel
new file mode 100644
index 0000000..d1afb1f
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/errwrap/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "errwrap",
+ srcs = [
+ "errwrap.go",
+ "functions.go",
+ ],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/errwrap",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//third_party/com_github_cockroachdb_cockroach/passesutil",
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ "@org_golang_x_tools//go/ast/inspector",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/errwrap/errwrap.go b/third_party/com_github_cockroachdb_cockroach/errwrap/errwrap.go
new file mode 100644
index 0000000..311334c
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/errwrap/errwrap.go
@@ -0,0 +1,205 @@
+// Copyright 2021 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package errwrap
+
+import (
+ "fmt"
+ "go/ast"
+ "go/constant"
+ "go/types"
+ "regexp"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+
+ "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/passesutil"
+)
+
+// Doc documents this pass.
+const Doc = `checks for unwrapped errors.
+
+This linter checks that:
+
+- err.Error() is not passed as an argument to an error-creating
+ function.
+
+- the '%s', '%v', and '%+v' format verbs are not used to format
+ errors when creating a new error.
+
+In both cases, an error-wrapping function can be used to correctly
+preserve the chain of errors so that user-directed hints, links to
+documentation issues, and telemetry data are all propagated.
+
+It is possible for a call site to opt the format/message string
+out of the linter using /* nolint:errwrap */ on or before the line
+that creates the error.`
+
+var errorType = types.Universe.Lookup("error").Type().String()
+
+// Analyzer checks for improperly wrapped errors.
+var Analyzer = &analysis.Analyzer{
+ Name: "errwrap",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspctr := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+
+ inspctr.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)
+ }
+ }()
+
+ callExpr, ok := n.(*ast.CallExpr)
+ if !ok {
+ return
+ }
+ if pass.TypesInfo.TypeOf(callExpr).String() != errorType {
+ return
+ }
+ sel, ok := callExpr.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return
+ }
+ obj, ok := pass.TypesInfo.Uses[sel.Sel]
+ if !ok {
+ return
+ }
+ fn, ok := obj.(*types.Func)
+ if !ok {
+ return
+ }
+ pkg := obj.Pkg()
+ if pkg == nil {
+ return
+ }
+
+ // Skip files generated by go-bindata.
+ file := pass.Fset.File(n.Pos())
+ if strings.HasSuffix(file.Name(), "/embedded.go") {
+ return
+ }
+ fnName := stripVendor(fn.FullName())
+
+ // Check that none of the arguments are err.Error()
+ if _, found := ErrorFnFormatStringIndex[fnName]; found {
+ for i := range callExpr.Args {
+ if isErrorStringCall(pass, callExpr.Args[i]) {
+ // If the argument is opting out of the linter with a special
+ // comment, tolerate that.
+ if passesutil.HasNolintComment(pass, sel, "errwrap") {
+ continue
+ }
+
+ pass.Report(analysis.Diagnostic{
+ Pos: n.Pos(),
+ Message: fmt.Sprintf(
+ "err.Error() is passed to %s.%s; use pgerror.Wrap/errors.Wrap/errors.CombineErrors/"+
+ "errors.WithSecondaryError/errors.NewAssertionErrorWithWrappedErrf instead",
+ pkg.Name(), fn.Name()),
+ })
+ }
+ }
+ }
+
+ // Check that the format string does not use %s or %v for an error.
+ formatStringIdx, ok := ErrorFnFormatStringIndex[fnName]
+ if !ok || formatStringIdx < 0 {
+ // Not an error formatting function.
+ return
+ }
+
+ // Find all % fields in the format string.
+ formatVerbs, ok := getFormatStringVerbs(pass, callExpr, formatStringIdx)
+ if !ok {
+ return
+ }
+
+ // For any arguments that are errors, check whether the wrapping verb
+ // is %s or %v.
+ args := callExpr.Args[formatStringIdx+1:]
+ for i := 0; i < len(args) && i < len(formatVerbs); i++ {
+ if pass.TypesInfo.TypeOf(args[i]).String() != errorType {
+ continue
+ }
+
+ if formatVerbs[i] == "%v" || formatVerbs[i] == "%+v" || formatVerbs[i] == "%s" {
+ // If the argument is opting out of the linter with a special
+ // comment, tolerate that.
+ if passesutil.HasNolintComment(pass, sel, "errwrap") {
+ continue
+ }
+
+ pass.Report(analysis.Diagnostic{
+ Pos: n.Pos(),
+ Message: fmt.Sprintf(
+ "non-wrapped error is passed to %s.%s; use pgerror.Wrap/errors.Wrap/errors.CombineErrors/"+
+ "errors.WithSecondaryError/errors.NewAssertionErrorWithWrappedErrf instead",
+ pkg.Name(), fn.Name(),
+ ),
+ })
+ }
+ }
+ })
+
+ return nil, nil
+}
+
+// isErrorStringCall tests whether the expression is a string expression that
+// is the result of an `(error).Error()` method call.
+func isErrorStringCall(pass *analysis.Pass, expr ast.Expr) bool {
+ if call, ok := expr.(*ast.CallExpr); ok {
+ if pass.TypesInfo.TypeOf(call).String() == "string" {
+ if callSel, ok := call.Fun.(*ast.SelectorExpr); ok {
+ fun := pass.TypesInfo.Uses[callSel.Sel].(*types.Func)
+ return fun.Type().String() == "func() string" && fun.Name() == "Error"
+ }
+ }
+ }
+ return false
+}
+
+// formatVerbRegexp naively matches format string verbs. This does not take
+// modifiers such as padding into account.
+var formatVerbRegexp = regexp.MustCompile(`%([^%+]|\+v)`)
+
+// getFormatStringVerbs return an array of all `%` format verbs from the format
+// string argument of a function call.
+// Based on https://github.com/polyfloyd/go-errorlint/blob/e4f368f0ae6983eb40821ba4f88dc84ac51aef5b/errorlint/lint.go#L88
+func getFormatStringVerbs(
+ pass *analysis.Pass, call *ast.CallExpr, formatStringIdx int,
+) ([]string, bool) {
+ if len(call.Args) <= formatStringIdx {
+ return nil, false
+ }
+ strLit, ok := call.Args[formatStringIdx].(*ast.BasicLit)
+ if !ok {
+ // Ignore format strings that are not literals.
+ return nil, false
+ }
+ formatString := constant.StringVal(pass.TypesInfo.Types[strLit].Value)
+
+ return formatVerbRegexp.FindAllString(formatString, -1), true
+}
+
+func stripVendor(s string) string {
+ if i := strings.Index(s, "/vendor/"); i != -1 {
+ s = s[i+len("/vendor/"):]
+ }
+ return s
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/errwrap/functions.go b/third_party/com_github_cockroachdb_cockroach/errwrap/functions.go
new file mode 100644
index 0000000..e482578
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/errwrap/functions.go
@@ -0,0 +1,20 @@
+// Copyright 2021 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package errwrap
+
+// ErrorFnFormatStringIndex contains functions that should be checked for
+// improperly wrapped errors. The value is the index of the function
+// parameter containing the format string. It is -1 if there is no format
+// string parameter.
+var ErrorFnFormatStringIndex = map[string]int{
+ "errors.New": -1,
+
+ "github.com/pkg/errors.New": -1,
+ "github.com/pkg/errors.Wrap": -1,
+
+ "fmt.Errorf": 0,
+
+ "github.com/pkg/errors.Errorf": 0,
+ "github.com/pkg/errors.Wrapf": 1,
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/fmtsafe/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/fmtsafe/BUILD.bazel
new file mode 100644
index 0000000..adebac2
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/fmtsafe/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "fmtsafe",
+ srcs = [
+ "fmtsafe.go",
+ "functions.go",
+ ],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/fmtsafe",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//third_party/com_github_cockroachdb_cockroach/errwrap",
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ "@org_golang_x_tools//go/ast/inspector",
+ "@org_golang_x_tools//go/types/typeutil",
+ ],
+)
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++")
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/fmtsafe/functions.go b/third_party/com_github_cockroachdb_cockroach/fmtsafe/functions.go
new file mode 100644
index 0000000..717fa35
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/fmtsafe/functions.go
@@ -0,0 +1,45 @@
+// Copyright 2020 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package fmtsafe
+
+import (
+ "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/errwrap"
+)
+
+// requireConstMsg records functions for which the last string
+// argument must be a constant string.
+var requireConstMsg = map[string]bool{}
+
+// requireConstFmt records functions for which the string arg
+// before the final ellipsis must be a constant string.
+var requireConstFmt = map[string]bool{
+ // Logging things.
+ "log.Printf": true,
+ "log.Fatalf": true,
+ "log.Panicf": true,
+ "(*log.Logger).Fatalf": true,
+ "(*log.Logger).Panicf": true,
+ "(*log.Logger).Printf": true,
+
+ "(go.etcd.io/etcd/raft/v3.Logger).Debugf": true,
+ "(go.etcd.io/etcd/raft/v3.Logger).Infof": true,
+ "(go.etcd.io/etcd/raft/v3.Logger).Warningf": true,
+ "(go.etcd.io/etcd/raft/v3.Logger).Errorf": true,
+ "(go.etcd.io/etcd/raft/v3.Logger).Fatalf": true,
+ "(go.etcd.io/etcd/raft/v3.Logger).Panicf": true,
+
+ "(google.golang.org/grpc/grpclog.Logger).Infof": true,
+ "(google.golang.org/grpc/grpclog.Logger).Warningf": true,
+ "(google.golang.org/grpc/grpclog.Logger).Errorf": true,
+}
+
+func init() {
+ for errorFn, formatStringIndex := range errwrap.ErrorFnFormatStringIndex {
+ if formatStringIndex < 0 {
+ requireConstMsg[errorFn] = true
+ } else {
+ requireConstFmt[errorFn] = true
+ }
+ }
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/hash/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/hash/BUILD.bazel
new file mode 100644
index 0000000..3583690
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/hash/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "hash",
+ srcs = ["hash.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/hash",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/hash/hash.go b/third_party/com_github_cockroachdb_cockroach/hash/hash.go
new file mode 100644
index 0000000..aac9ab7
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/hash/hash.go
@@ -0,0 +1,139 @@
+// Copyright 2016 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package hash defines an Analyzer that detects correct use of hash.Hash.
+package hash
+
+import (
+ "go/ast"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+)
+
+// Doc documents this pass.
+const Doc = `check for correct use of hash.Hash`
+
+// Analyzer defines this pass.
+var Analyzer = &analysis.Analyzer{
+ Name: "hash",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+// hashChecker assures that the hash.Hash interface is not misused. A common
+// mistake is to assume that the Sum function returns the hash of its input,
+// like so:
+//
+// hashedBytes := sha256.New().Sum(inputBytes)
+//
+// In fact, the parameter to Sum is not the bytes to be hashed, but a slice that
+// will be used as output in case the caller wants to avoid an allocation. In
+// the example above, hashedBytes is not the SHA-256 hash of inputBytes, but
+// the concatenation of inputBytes with the hash of the empty string.
+//
+// Correct uses of the hash.Hash interface are as follows:
+//
+// h := sha256.New()
+// h.Write(inputBytes)
+// hashedBytes := h.Sum(nil)
+//
+// h := sha256.New()
+// h.Write(inputBytes)
+// var hashedBytes [sha256.Size]byte
+// h.Sum(hashedBytes[:0])
+//
+// To differentiate between correct and incorrect usages, hashChecker applies a
+// simple heuristic: it flags calls to Sum where a) the parameter is non-nil and
+// b) the return value is used.
+//
+// The hash.Hash interface may be remedied in Go 2. See golang/go#21070.
+func run(pass *analysis.Pass) (interface{}, error) {
+ selectorIsHash := func(s *ast.SelectorExpr) bool {
+ tv, ok := pass.TypesInfo.Types[s.X]
+ if !ok {
+ return false
+ }
+ named, ok := tv.Type.(*types.Named)
+ if !ok {
+ return false
+ }
+ if named.Obj().Type().String() != "hash.Hash" {
+ return false
+ }
+ return true
+ }
+
+ stack := make([]ast.Node, 0, 32)
+ forAllFiles(pass.Files, func(n ast.Node) bool {
+ if n == nil {
+ stack = stack[:len(stack)-1] // pop
+ return true
+ }
+ stack = append(stack, n) // push
+
+ // Find a call to hash.Hash.Sum.
+ selExpr, ok := n.(*ast.SelectorExpr)
+ if !ok {
+ return true
+ }
+ if selExpr.Sel.Name != "Sum" {
+ return true
+ }
+ if !selectorIsHash(selExpr) {
+ return true
+ }
+ callExpr, ok := stack[len(stack)-2].(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ if len(callExpr.Args) != 1 {
+ return true
+ }
+ // We have a valid call to hash.Hash.Sum.
+
+ // Is the argument nil?
+ var nilArg bool
+ if id, ok := callExpr.Args[0].(*ast.Ident); ok && id.Name == "nil" {
+ nilArg = true
+ }
+
+ // Is the return value unused?
+ var retUnused bool
+ Switch:
+ switch t := stack[len(stack)-3].(type) {
+ case *ast.AssignStmt:
+ for i := range t.Rhs {
+ if t.Rhs[i] == stack[len(stack)-2] {
+ if id, ok := t.Lhs[i].(*ast.Ident); ok && id.Name == "_" {
+ // Assigning to the blank identifier does not count as using the
+ // return value.
+ retUnused = true
+ }
+ break Switch
+ }
+ }
+ panic("unreachable")
+ case *ast.ExprStmt:
+ // An expression statement means the return value is unused.
+ retUnused = true
+ default:
+ }
+
+ if !nilArg && !retUnused {
+ pass.Reportf(callExpr.Pos(), "probable misuse of hash.Hash.Sum: "+
+ "provide parameter or use return value, but not both")
+ }
+ return true
+ })
+
+ return nil, nil
+}
+
+func forAllFiles(files []*ast.File, fn func(node ast.Node) bool) {
+ for _, f := range files {
+ ast.Inspect(f, fn)
+ }
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/nilness/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/nilness/BUILD.bazel
new file mode 100644
index 0000000..6640668
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/nilness/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "nilness",
+ srcs = ["nilness.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/nilness",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/buildssa",
+ "@org_golang_x_tools//go/ssa",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/nilness/nilness.go b/third_party/com_github_cockroachdb_cockroach/nilness/nilness.go
new file mode 100644
index 0000000..58f8c9a
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/nilness/nilness.go
@@ -0,0 +1,416 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Portions of this file are additionally subject to the following
+// license and copyright.
+//
+// Copyright 2021 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package nilness inspects the control-flow graph of an SSA function
+// and reports errors such as nil pointer dereferences and degenerate
+// nil pointer comparisons.
+package nilness
+
+import (
+ "fmt"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/buildssa"
+ "golang.org/x/tools/go/ssa"
+)
+
+const doc = `check for redundant or impossible nil comparisons
+
+The nilness checker inspects the control-flow graph of each function in
+a package and reports nil pointer dereferences, degenerate nil
+pointers, and panics with nil values. A degenerate comparison is of the form
+x==nil or x!=nil where x is statically known to be nil or non-nil. These are
+often a mistake, especially in control flow related to errors. Panics with nil
+values are checked because they are not detectable by
+
+ if r := recover(); r != nil {
+
+This check reports conditions such as:
+
+ if f == nil { // impossible condition (f is a function)
+ }
+
+and:
+
+ p := &v
+ ...
+ if p != nil { // tautological condition
+ }
+
+and:
+
+ if p == nil {
+ print(*p) // nil dereference
+ }
+
+and:
+
+ if p == nil {
+ panic(p)
+ }
+`
+
+type argExtractor func(c *ssa.CallCommon) ssa.Value
+
+type analyzerConfig struct {
+ // reportDegenerateConditions controls the reporting of "impossible"
+ // and "tautological" comparisons. While many of these are in error,
+ // it also catches nil checks where the current control flow graph
+ // can't produce nil but where we might want to guard against changes
+ // in the future. It also catches a number of compound conditionals
+ // where the "tautological" check produces more readable code.
+ reportDegenerateIfConditions bool
+ // argExtractors is a map from the full name of a function to
+ // a func that returns the argument from a Call that should
+ // not be passed provably nil values.
+ argExtractors map[string]argExtractor
+}
+
+var crdbConfig = analyzerConfig{
+ reportDegenerateIfConditions: false,
+ argExtractors: map[string]argExtractor{},
+}
+
+var defaultConfig = analyzerConfig{
+ reportDegenerateIfConditions: true,
+}
+
+// Analyzer defines a pass that checks for uses of provably nil
+// values that were likely in error with custom configuration
+// suitable for the CRDB codebase.
+var Analyzer = &analysis.Analyzer{
+ Name: "nilness",
+ Doc: doc,
+ Run: crdbConfig.run,
+ Requires: []*analysis.Analyzer{buildssa.Analyzer},
+}
+
+// TestAnalyzer defines a pass that checks for uses of provably nil values
+// that were likely in error.
+var TestAnalyzer = &analysis.Analyzer{
+ Name: "nilness",
+ Doc: doc,
+ Run: defaultConfig.run,
+ Requires: []*analysis.Analyzer{buildssa.Analyzer},
+}
+
+func (a *analyzerConfig) run(pass *analysis.Pass) (interface{}, error) {
+ ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+ for _, fn := range ssainput.SrcFuncs {
+ a.runFunc(pass, fn)
+ }
+ return nil, nil
+}
+
+func (a *analyzerConfig) runFunc(pass *analysis.Pass, fn *ssa.Function) {
+ reportf := func(category string, pos token.Pos, format string, args ...interface{}) {
+ pass.Report(analysis.Diagnostic{
+ Pos: pos,
+ Category: category,
+ Message: fmt.Sprintf(format, args...),
+ })
+ }
+
+ // notNil reports an error if v is provably nil.
+ notNil := func(stack []fact, instr ssa.Instruction, v ssa.Value, descr string) {
+ if nilnessOf(stack, v) == isnil {
+ reportf("nilderef", instr.Pos(), "nil dereference in "+descr)
+ }
+ }
+
+ // notNilArg reports an error if v is provably nil.
+ notNilArg := func(stack []fact, instr ssa.Instruction, v ssa.Value, descr string) {
+ if nilnessOf(stack, v) == isnil {
+ reportf("nilarg", instr.Pos(), "nil argument to "+descr)
+ }
+ }
+
+ // visit visits reachable blocks of the CFG in dominance order,
+ // maintaining a stack of dominating nilness facts.
+ //
+ // By traversing the dom tree, we can pop facts off the stack as
+ // soon as we've visited a subtree. Had we traversed the CFG,
+ // we would need to retain the set of facts for each block.
+ seen := make([]bool, len(fn.Blocks)) // seen[i] means visit should ignore block i
+ var visit func(b *ssa.BasicBlock, stack []fact)
+ visit = func(b *ssa.BasicBlock, stack []fact) {
+ if seen[b.Index] {
+ return
+ }
+ seen[b.Index] = true
+
+ // Report nil dereferences.
+ for _, instr := range b.Instrs {
+ switch instr := instr.(type) {
+ case ssa.CallInstruction:
+ call := instr.Common()
+ notNil(stack, instr, call.Value, call.Description())
+
+ switch f := call.Value.(type) {
+ case *ssa.Function:
+ if tf, ok := f.Object().(*types.Func); ok {
+ if extract, ok := a.argExtractors[tf.FullName()]; ok {
+ notNilArg(stack, instr, extract(call), tf.FullName())
+ }
+ }
+ }
+ case *ssa.FieldAddr:
+ notNil(stack, instr, instr.X, "field selection")
+ case *ssa.IndexAddr:
+ notNil(stack, instr, instr.X, "index operation")
+ case *ssa.MapUpdate:
+ notNil(stack, instr, instr.Map, "map update")
+ case *ssa.Slice:
+ // A nilcheck occurs in ptr[:] iff ptr is a pointer to an array.
+ if _, ok := instr.X.Type().Underlying().(*types.Pointer); ok {
+ notNil(stack, instr, instr.X, "slice operation")
+ }
+ case *ssa.Store:
+ notNil(stack, instr, instr.Addr, "store")
+ case *ssa.TypeAssert:
+ if !instr.CommaOk {
+ notNil(stack, instr, instr.X, "type assertion")
+ }
+ case *ssa.UnOp:
+ if instr.Op == token.MUL { // *X
+ notNil(stack, instr, instr.X, "load")
+ }
+ }
+ }
+
+ // Look for panics with nil value
+ for _, instr := range b.Instrs {
+ switch instr := instr.(type) {
+ case *ssa.Panic:
+ if nilnessOf(stack, instr.X) == isnil {
+ reportf("nilpanic", instr.Pos(), "panic with nil value")
+ }
+ }
+ }
+
+ // For nil comparison blocks, report an error if the condition
+ // is degenerate, and push a nilness fact on the stack when
+ // visiting its true and false successor blocks.
+ if binop, tsucc, fsucc := eq(b); binop != nil {
+ xnil := nilnessOf(stack, binop.X)
+ ynil := nilnessOf(stack, binop.Y)
+
+ if ynil != unknown && xnil != unknown && (xnil == isnil || ynil == isnil) {
+ // Degenerate condition:
+ // the nilness of both operands is known,
+ // and at least one of them is nil.
+ if a.reportDegenerateIfConditions {
+ var adj string
+ if (xnil == ynil) == (binop.Op == token.EQL) {
+ adj = "tautological"
+ } else {
+ adj = "impossible"
+ }
+ reportf("cond", binop.Pos(), "%s condition: %s %s %s", adj, xnil, binop.Op, ynil)
+ }
+
+ // If tsucc's or fsucc's sole incoming edge is impossible,
+ // it is unreachable. Prune traversal of it and
+ // all the blocks it dominates.
+ // (We could be more precise with full dataflow
+ // analysis of control-flow joins.)
+ var skip *ssa.BasicBlock
+ if xnil == ynil {
+ skip = fsucc
+ } else {
+ skip = tsucc
+ }
+ for _, d := range b.Dominees() {
+ if d == skip && len(d.Preds) == 1 {
+ continue
+ }
+ visit(d, stack)
+ }
+ return
+ }
+
+ // "if x == nil" or "if nil == y" condition; x, y are unknown.
+ if xnil == isnil || ynil == isnil {
+ var newFacts facts
+ if xnil == isnil {
+ // x is nil, y is unknown:
+ // t successor learns y is nil.
+ newFacts = expandFacts(fact{binop.Y, isnil})
+ } else {
+ // x is nil, y is unknown:
+ // t successor learns x is nil.
+ newFacts = expandFacts(fact{binop.X, isnil})
+ }
+
+ for _, d := range b.Dominees() {
+ // Successor blocks learn a fact
+ // only at non-critical edges.
+ // (We could do be more precise with full dataflow
+ // analysis of control-flow joins.)
+ s := stack
+ if len(d.Preds) == 1 {
+ if d == tsucc {
+ s = append(s, newFacts...)
+ } else if d == fsucc {
+ s = append(s, newFacts.negate()...)
+ }
+ }
+ visit(d, s)
+ }
+ return
+ }
+ }
+
+ for _, d := range b.Dominees() {
+ visit(d, stack)
+ }
+ }
+
+ // Visit the entry block. No need to visit fn.Recover.
+ if fn.Blocks != nil {
+ visit(fn.Blocks[0], make([]fact, 0, 20)) // 20 is plenty
+ }
+}
+
+// A fact records that a block is dominated
+// by the condition v == nil or v != nil.
+type fact struct {
+ value ssa.Value
+ nilness nilness
+}
+
+func (f fact) negate() fact { return fact{f.value, -f.nilness} }
+
+type nilness int
+
+const (
+ isnonnil = -1
+ unknown nilness = 0
+ isnil = 1
+)
+
+var nilnessStrings = []string{"non-nil", "unknown", "nil"}
+
+func (n nilness) String() string { return nilnessStrings[n+1] }
+
+// nilnessOf reports whether v is definitely nil, definitely not nil,
+// or unknown given the dominating stack of facts.
+func nilnessOf(stack []fact, v ssa.Value) nilness {
+ switch v := v.(type) {
+ // unwrap ChangeInterface values recursively, to detect if underlying
+ // values have any facts recorded or are otherwise known with regard to nilness.
+ //
+ // This work must be in addition to expanding facts about
+ // ChangeInterfaces during inference/fact gathering because this covers
+ // cases where the nilness of a value is intrinsic, rather than based
+ // on inferred facts, such as a zero value interface variable. That
+ // said, this work alone would only inform us when facts are about
+ // underlying values, rather than outer values, when the analysis is
+ // transitive in both directions.
+ case *ssa.ChangeInterface:
+ if underlying := nilnessOf(stack, v.X); underlying != unknown {
+ return underlying
+ }
+ }
+
+ // Is value intrinsically nil or non-nil?
+ switch v := v.(type) {
+ case *ssa.Alloc,
+ *ssa.FieldAddr,
+ *ssa.FreeVar,
+ *ssa.Function,
+ *ssa.Global,
+ *ssa.IndexAddr,
+ *ssa.MakeChan,
+ *ssa.MakeClosure,
+ *ssa.MakeInterface,
+ *ssa.MakeMap,
+ *ssa.MakeSlice:
+ return isnonnil
+ case *ssa.Const:
+ if v.IsNil() {
+ return isnil
+ }
+ return isnonnil
+ }
+
+ // Search dominating control-flow facts.
+ for _, f := range stack {
+ if f.value == v {
+ return f.nilness
+ }
+ }
+ return unknown
+}
+
+// If b ends with an equality comparison, eq returns the operation and
+// its true (equal) and false (not equal) successors.
+func eq(b *ssa.BasicBlock) (op *ssa.BinOp, tsucc, fsucc *ssa.BasicBlock) {
+ if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok {
+ if binop, ok := If.Cond.(*ssa.BinOp); ok {
+ switch binop.Op {
+ case token.EQL:
+ return binop, b.Succs[0], b.Succs[1]
+ case token.NEQ:
+ return binop, b.Succs[1], b.Succs[0]
+ }
+ }
+ }
+ return nil, nil, nil
+}
+
+// expandFacts takes a single fact and returns the set of facts that can be
+// known about it or any of its related values. Some operations, like
+// ChangeInterface, have transitive nilness, such that if you know the
+// underlying value is nil, you also know the value itself is nil, and vice
+// versa. This operation allows callers to match on any of the related values
+// in analyzes, rather than just the one form of the value that happened to
+// appear in a comparison.
+//
+// This work must be in addition to unwrapping values within nilnessOf because
+// while this work helps give facts about transitively known values based on
+// inferred facts, the recursive check within nilnessOf covers cases where
+// nilness facts are intrinsic to the underlying value, such as a zero value
+// interface variables.
+//
+// ChangeInterface is the only expansion currently supported, but others, like
+// Slice, could be added. At this time, this tool does not check slice
+// operations in a way this expansion could help. See
+// https://play.golang.org/p/mGqXEp7w4fR for an example.
+func expandFacts(f fact) []fact {
+ ff := []fact{f}
+
+Loop:
+ for {
+ switch v := f.value.(type) {
+ case *ssa.ChangeInterface:
+ f = fact{v.X, f.nilness}
+ ff = append(ff, f)
+ default:
+ break Loop
+ }
+ }
+
+ return ff
+}
+
+type facts []fact
+
+func (ff facts) negate() facts {
+ nn := make([]fact, len(ff))
+ for i, f := range ff {
+ nn[i] = f.negate()
+ }
+ return nn
+}
+
+var firstArg = func(c *ssa.CallCommon) ssa.Value { return c.Args[0] }
diff --git a/third_party/com_github_cockroachdb_cockroach/passesutil/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/passesutil/BUILD.bazel
new file mode 100644
index 0000000..3295800
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/passesutil/BUILD.bazel
@@ -0,0 +1,12 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "passesutil",
+ srcs = ["passes_util.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/passesutil",
+ visibility = ["//visibility:public"],
+ deps = [
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/ast/astutil",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/passesutil/passes_util.go b/third_party/com_github_cockroachdb_cockroach/passesutil/passes_util.go
new file mode 100644
index 0000000..681e1eb
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/passesutil/passes_util.go
@@ -0,0 +1,127 @@
+// Copyright 2020 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package passesutil provides useful functionality for implementing passes.
+package passesutil
+
+import (
+ "fmt"
+ "go/ast"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/ast/astutil"
+)
+
+// FindContainingFile finds the file for an ast Node in a Pass.
+func FindContainingFile(pass *analysis.Pass, n ast.Node) *ast.File {
+ fPos := pass.Fset.File(n.Pos())
+ for _, f := range pass.Files {
+ if pass.Fset.File(f.Pos()) == fPos {
+ return f
+ }
+ }
+ panic(fmt.Errorf("cannot file file for %v", n))
+}
+
+// HasNolintComment returns true if the comments on the passed node have
+// the comment "nolint:<nolintName>" appearing anywhere in it.
+func HasNolintComment(pass *analysis.Pass, n ast.Node, nolintName string) bool {
+ f := FindContainingFile(pass, n)
+ relevant, containing := findNodesInBlock(f, n)
+ cm := ast.NewCommentMap(pass.Fset, containing, f.Comments)
+ // Check to see if any of the relevant ast nodes contain the relevant comment.
+ nolintComment := "nolint:" + nolintName
+ for _, cn := range relevant {
+ // Ident nodes have all comments on the ident in containing. This may be
+ // too many. Imagine there is a comment on the ident somewhere else in the
+ // decl block, we wouldn't want that to affect a later conversion.
+ //
+ // We want to filter this down to all comments on the ident inside
+ // the relevant statement. To do this we reject comments on idents which
+ // have their slash outside the outermost relevant node. We do this
+ // by inspecting the position of the comment relative to the outermost
+ // relevant node. This is sound because we can't have a comment on an ident
+ // alone as an ident is not a statement.
+ _, isIdent := cn.(*ast.Ident)
+ for _, cg := range cm[cn] {
+ for _, c := range cg.List {
+ if !strings.Contains(c.Text, nolintComment) {
+ continue
+ }
+ outermost := relevant[len(relevant)-1]
+ if isIdent && (cg.Pos() < outermost.Pos() || cg.End() > outermost.End()) {
+ continue
+ }
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// findNodesInBlock finds all expressions and statements that occur underneath
+// the block or decl closest to n. The idea is that we want to find comments
+// which occur in in the block or decl which includes the ast node n for
+// filtering. We want to filter the comments down to all comments
+// which are associated with n or any expression up to a statement in the
+// closest enclosing block or decl. This is to deal with multi-line
+// expressions or with cases where the relevant expression occurs in an
+// init clause of an if or for and the comment is on the preceding line.
+//
+// For example, imagine that n is the *ast.CallExpr on the method foo in the
+// following snippet:
+//
+// func nonsense() bool {
+// if v := (g.foo() + 1) > 2; !v {
+// return true
+// }
+// return false
+// }
+//
+// This function would return all of the nodes up to the `IfStmt` as relevant
+// and would return the `BlockStmt` of the function nonsense as containing.
+func findNodesInBlock(f *ast.File, n ast.Node) (relevant []ast.Node, containing ast.Node) {
+ stack, _ := astutil.PathEnclosingInterval(f, n.Pos(), n.End())
+ // Add all of the children of n to the set of relevant nodes.
+ ast.Walk(funcVisitor(func(node ast.Node) {
+ relevant = append(relevant, node)
+ }), n)
+
+ // Nodes were just added with the parent at the beginning and children at the
+ // end. Reverse it.
+ reverseNodes(relevant)
+
+ // Add the parents up to the enclosing block or declaration block and discover
+ // that containing node.
+ containing = f // worst-case
+ for _, n := range stack {
+ switch n.(type) {
+ case *ast.GenDecl, *ast.BlockStmt:
+ containing = n
+ return relevant, containing
+ default:
+ // Add all of the parents of n up to the containing BlockStmt or GenDecl
+ // to the set of relevant nodes.
+ relevant = append(relevant, n)
+ }
+ }
+ return relevant, containing
+}
+
+func reverseNodes(n []ast.Node) {
+ for i := 0; i < len(n)/2; i++ {
+ n[i], n[len(n)-i-1] = n[len(n)-i-1], n[i]
+ }
+}
+
+type funcVisitor func(node ast.Node)
+
+var _ ast.Visitor = (funcVisitor)(nil)
+
+func (f funcVisitor) Visit(node ast.Node) (w ast.Visitor) {
+ if node != nil {
+ f(node)
+ }
+ return f
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/returnerrcheck/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/returnerrcheck/BUILD.bazel
new file mode 100644
index 0000000..80f0ce8
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/returnerrcheck/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "returnerrcheck",
+ srcs = ["returnerrcheck.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/returnerrcheck",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//third_party/com_github_cockroachdb_cockroach/passesutil",
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ "@org_golang_x_tools//go/ast/astutil",
+ "@org_golang_x_tools//go/ast/inspector",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/returnerrcheck/returnerrcheck.go b/third_party/com_github_cockroachdb_cockroach/returnerrcheck/returnerrcheck.go
new file mode 100644
index 0000000..85000ca
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/returnerrcheck/returnerrcheck.go
@@ -0,0 +1,207 @@
+// Copyright 2019 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package returnerrcheck defines an suite of Analyzers that
+// detects conditionals which check for a non-nil error and then
+// proceed to return a nil error.
+package returnerrcheck
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/ast/inspector"
+
+ "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/passesutil"
+)
+
+// Doc documents this pass.
+const Doc = `check for return of nil in a conditional which check for non-nil errors`
+
+var errorType = types.Universe.Lookup("error").Type()
+
+const name = "returnerrcheck"
+
+// Analyzer is a linter that ensures that returns from functions which
+// return an error as their last return value which have returns inside
+// the body of if conditionals which check for an error to be non-nil do
+// not return a nil error without a `//nolint:returnerrcheck` comment.
+var Analyzer = &analysis.Analyzer{
+ Name: name,
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: func(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ inspect.Preorder([]ast.Node{
+ (*ast.IfStmt)(nil),
+ }, func(n ast.Node) {
+ ifStmt := n.(*ast.IfStmt)
+
+ // Are we inside of a function which returns an error?
+ containing := findContainingFunc(pass, n)
+ if !funcReturnsErrorLast(containing) {
+ return
+ }
+
+ // Now we want to know if the condition in this statement checks if
+ // an error is non-nil.
+ errObj, ok := isNonNilErrCheck(pass, ifStmt.Cond)
+ if !ok {
+ return
+ }
+ for i, n := range ifStmt.Body.List {
+ returnStmt, ok := n.(*ast.ReturnStmt)
+ if !ok {
+ continue
+ }
+ if len(returnStmt.Results) == 0 {
+ continue
+ }
+ lastRes := returnStmt.Results[len(returnStmt.Results)-1]
+ if !pass.TypesInfo.Types[lastRes].IsNil() {
+ continue
+ }
+ if hasAcceptableAction(pass, errObj, ifStmt.Body.List[:i+1]) {
+ continue
+ }
+ if passesutil.HasNolintComment(pass, returnStmt, name) {
+ continue
+ }
+ pass.Report(analysis.Diagnostic{
+ Pos: n.Pos(),
+ Message: fmt.Sprintf("unexpected nil error return after checking for a non-nil error" +
+ "; if this is not a mistake, add a \"//nolint:returnerrcheck\" comment"),
+ })
+ }
+ })
+ return nil, nil
+ },
+}
+
+// hasAcceptableAction determines if there is some action in statements which
+// uses errObj in a way which excuses a nil error return value. Such actions
+// include assigning the error to a value, calling Error() on it, type asserting
+// it, calling a function with the error as an argument, or returning the error
+// in a different return statement.
+func hasAcceptableAction(pass *analysis.Pass, errObj types.Object, statements []ast.Stmt) bool {
+ var seen bool
+ isObj := func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok {
+ if seen = pass.TypesInfo.Uses[id] == errObj; seen {
+ return true
+ }
+ }
+ return false
+ }
+ inspect := func(n ast.Node) (wantMore bool) {
+ switch n := n.(type) {
+ case *ast.CallExpr:
+ // If we call a function with the non-nil error object, we probably know
+ // what we're doing.
+ for _, arg := range n.Args {
+ if isObj(arg) {
+ return false
+ }
+ }
+ case *ast.AssignStmt:
+ // If we assign the error to some value, we probably know what we're
+ // doing.
+ for _, rhs := range n.Rhs {
+ if isObj(rhs) {
+ return false
+ }
+ }
+ case *ast.KeyValueExpr:
+ // If we assign the error to a field or map in a literal, we probably
+ // know what we're doing.
+ if isObj(n.Value) {
+ return false
+ }
+
+ case *ast.SelectorExpr:
+ // If we're selecting something off of the error (i.e. err.Error()) then
+ // we probably know what we're doing.
+ if isObj(n.X) {
+ return false
+ }
+ case *ast.ReturnStmt:
+ // If we return the error, perhaps in a different if statement, we
+ // might know what we're doing. This is good for cases like:
+ //
+ // if cond || err != nil {
+ // if err != nil {
+ // return err
+ // }
+ // ...
+ // return nil
+ // }
+ //
+ // TODO(ajwerner): ensure that this is returning from the right
+ // function or whether this is a good idea.
+ if numRes := len(n.Results); numRes > 0 && isObj(n.Results[numRes-1]) {
+ return false
+ }
+ }
+ return true
+ }
+ for _, stmt := range statements {
+ if ast.Inspect(stmt, inspect); seen {
+ return true
+ }
+ }
+ return false
+}
+
+func isNonNilErrCheck(pass *analysis.Pass, expr ast.Expr) (errObj types.Object, ok bool) {
+ // TODO(ajwerner): Maybe make this understand !(err == nil) and its variants.
+ binaryExpr, ok := expr.(*ast.BinaryExpr)
+ if !ok {
+ return nil, false
+ }
+ // We care about cases when errors are idents not when they're fields
+ switch binaryExpr.Op {
+ case token.NEQ:
+ var id *ast.Ident
+ if pass.TypesInfo.Types[binaryExpr.X].Type == errorType &&
+ pass.TypesInfo.Types[binaryExpr.Y].IsNil() {
+ id, ok = binaryExpr.X.(*ast.Ident)
+ } else if pass.TypesInfo.Types[binaryExpr.Y].Type == errorType &&
+ pass.TypesInfo.Types[binaryExpr.X].IsNil() {
+ id, ok = binaryExpr.Y.(*ast.Ident)
+ }
+ if ok {
+ errObj, ok := pass.TypesInfo.Uses[id]
+ return errObj, ok
+ }
+ case token.LAND, token.LOR:
+ if errObj, ok := isNonNilErrCheck(pass, binaryExpr.X); ok {
+ return errObj, ok
+ }
+ return isNonNilErrCheck(pass, binaryExpr.Y)
+ }
+ return nil, false
+}
+
+func funcReturnsErrorLast(f *types.Signature) bool {
+ results := f.Results()
+ return results.Len() > 0 &&
+ results.At(results.Len()-1).Type() == errorType
+}
+
+func findContainingFunc(pass *analysis.Pass, n ast.Node) *types.Signature {
+ stack, _ := astutil.PathEnclosingInterval(passesutil.FindContainingFile(pass, n), n.Pos(), n.End())
+ for _, n := range stack {
+ if funcDecl, ok := n.(*ast.FuncDecl); ok {
+ return pass.TypesInfo.ObjectOf(funcDecl.Name).(*types.Func).Type().(*types.Signature)
+ }
+ if funcLit, ok := n.(*ast.FuncLit); ok {
+ return pass.TypesInfo.Types[funcLit].Type.(*types.Signature)
+ }
+ }
+ return nil
+}
diff --git a/third_party/com_github_cockroachdb_cockroach/unconvert/BUILD.bazel b/third_party/com_github_cockroachdb_cockroach/unconvert/BUILD.bazel
new file mode 100644
index 0000000..9e42ebf
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/unconvert/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+filegroup(
+ name = "testdata",
+ srcs = glob(["testdata/**"]),
+ visibility = ["//pkg/testutils/lint/passes:__subpackages__"],
+)
+
+go_library(
+ name = "unconvert",
+ srcs = ["unconvert.go"],
+ importpath = "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/unconvert",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//third_party/com_github_cockroachdb_cockroach/passesutil",
+ "@org_golang_x_tools//go/analysis",
+ "@org_golang_x_tools//go/analysis/passes/inspect",
+ "@org_golang_x_tools//go/ast/inspector",
+ ],
+)
diff --git a/third_party/com_github_cockroachdb_cockroach/unconvert/unconvert.go b/third_party/com_github_cockroachdb_cockroach/unconvert/unconvert.go
new file mode 100644
index 0000000..4a6655b
--- /dev/null
+++ b/third_party/com_github_cockroachdb_cockroach/unconvert/unconvert.go
@@ -0,0 +1,165 @@
+// Copyright 2016 The Cockroach Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package unconvert defines an Analyzer that detects unnecessary type
+// conversions.
+package unconvert
+
+import (
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+
+ "source.monogon.dev/third_party/com_github_cockroachdb_cockroach/passesutil"
+)
+
+// Doc documents this pass.
+const Doc = `check for unnecessary type conversions`
+
+const name = "unconvert"
+
+// Analyzer defines this pass.
+var Analyzer = &analysis.Analyzer{
+ Name: name,
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Run: run,
+}
+
+// Adapted from https://github.com/mdempsky/unconvert/blob/beb68d938016d2dec1d1b078054f4d3db25f97be/unconvert.go#L371-L414.
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return
+ }
+ if len(call.Args) != 1 || call.Ellipsis != token.NoPos {
+ return
+ }
+ ft, ok := pass.TypesInfo.Types[call.Fun]
+ if !ok {
+ pass.Reportf(call.Pos(), "missing type")
+ return
+ }
+ if !ft.IsType() {
+ // Function call; not a conversion.
+ return
+ }
+ at, ok := pass.TypesInfo.Types[call.Args[0]]
+ if !ok {
+ pass.Reportf(call.Pos(), "missing type")
+ return
+ }
+ if !types.Identical(ft.Type, at.Type) {
+ // A real conversion.
+ return
+ }
+ if isUntypedValue(call.Args[0], pass.TypesInfo) {
+ // Workaround golang.org/issue/13061.
+ return
+ }
+ // Adapted from https://github.com/mdempsky/unconvert/blob/beb68d938016d2dec1d1b078054f4d3db25f97be/unconvert.go#L416-L430.
+ //
+ // cmd/cgo generates explicit type conversions that
+ // are often redundant when introducing
+ // _cgoCheckPointer calls (issue #16). Users can't do
+ // anything about these, so skip over them.
+ if ident, ok := call.Fun.(*ast.Ident); ok {
+ if ident.Name == "_cgoCheckPointer" {
+ return
+ }
+ }
+ if passesutil.HasNolintComment(pass, call, name) {
+ return
+ }
+ pass.Reportf(call.Pos(), "unnecessary conversion")
+ })
+
+ return nil, nil
+}
+
+// Cribbed from https://github.com/mdempsky/unconvert/blob/beb68d938016d2dec1d1b078054f4d3db25f97be/unconvert.go#L557-L607.
+func isUntypedValue(n ast.Expr, info *types.Info) bool {
+ switch n := n.(type) {
+ case *ast.BinaryExpr:
+ switch n.Op {
+ case token.SHL, token.SHR:
+ // Shifts yield an untyped value if their LHS is untyped.
+ return isUntypedValue(n.X, info)
+ case token.EQL, token.NEQ, token.LSS, token.GTR, token.LEQ, token.GEQ:
+ // Comparisons yield an untyped boolean value.
+ return true
+ case token.ADD, token.SUB, token.MUL, token.QUO, token.REM,
+ token.AND, token.OR, token.XOR, token.AND_NOT,
+ token.LAND, token.LOR:
+ return isUntypedValue(n.X, info) && isUntypedValue(n.Y, info)
+ }
+ case *ast.UnaryExpr:
+ switch n.Op {
+ case token.ADD, token.SUB, token.NOT, token.XOR:
+ return isUntypedValue(n.X, info)
+ }
+ case *ast.BasicLit:
+ // Basic literals are always untyped.
+ return true
+ case *ast.ParenExpr:
+ return isUntypedValue(n.X, info)
+ case *ast.SelectorExpr:
+ return isUntypedValue(n.Sel, info)
+ case *ast.Ident:
+ if obj, ok := info.Uses[n]; ok {
+ if obj.Pkg() == nil && obj.Name() == "nil" {
+ // The universal untyped zero value.
+ return true
+ }
+ if b, ok := obj.Type().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 {
+ // Reference to an untyped constant.
+ return true
+ }
+ }
+ case *ast.CallExpr:
+ if b, ok := asBuiltin(n.Fun, info); ok {
+ switch b.Name() {
+ case "real", "imag":
+ return isUntypedValue(n.Args[0], info)
+ case "complex":
+ return isUntypedValue(n.Args[0], info) && isUntypedValue(n.Args[1], info)
+ }
+ }
+ }
+
+ return false
+}
+
+// Cribbed from https://github.com/mdempsky/unconvert/blob/beb68d938016d2dec1d1b078054f4d3db25f97be/unconvert.go#L609-L630.
+func asBuiltin(n ast.Expr, info *types.Info) (*types.Builtin, bool) {
+ for {
+ paren, ok := n.(*ast.ParenExpr)
+ if !ok {
+ break
+ }
+ n = paren.X
+ }
+
+ ident, ok := n.(*ast.Ident)
+ if !ok {
+ return nil, false
+ }
+
+ obj, ok := info.Uses[ident]
+ if !ok {
+ return nil, false
+ }
+
+ b, ok := obj.(*types.Builtin)
+ return b, ok
+}