| Tim Windelschmidt | 1f4590b | 2025-07-29 23:05:36 +0200 | [diff] [blame^] | 1 | // Copyright 2020 The Cockroach Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| 4 | // Package errcmp defines an Analyzer which checks |
| 5 | // for usage of errors.Is instead of direct ==/!= comparisons. |
| 6 | package errcmp |
| 7 | |
| 8 | import ( |
| 9 | "go/ast" |
| 10 | "go/token" |
| 11 | "go/types" |
| 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 | |
| 19 | // Doc documents this pass. |
| 20 | const Doc = `check for comparison of error objects` |
| 21 | |
| 22 | var errorType = types.Universe.Lookup("error").Type() |
| 23 | |
| 24 | // Analyzer checks for usage of errors.Is instead of direct ==/!= |
| 25 | // comparisons. |
| 26 | var Analyzer = &analysis.Analyzer{ |
| 27 | Name: "errcmp", |
| 28 | Doc: Doc, |
| 29 | Requires: []*analysis.Analyzer{inspect.Analyzer}, |
| 30 | Run: run, |
| 31 | } |
| 32 | |
| 33 | func run(pass *analysis.Pass) (interface{}, error) { |
| 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) |
| 35 | |
| 36 | // Our analyzer just wants to see comparisons and casts. |
| 37 | nodeFilter := []ast.Node{ |
| 38 | (*ast.BinaryExpr)(nil), |
| 39 | (*ast.TypeAssertExpr)(nil), |
| 40 | (*ast.SwitchStmt)(nil), |
| 41 | } |
| 42 | |
| 43 | // Now traverse the ASTs. |
| 44 | inspect.Preorder(nodeFilter, func(n ast.Node) { |
| 45 | // Catch-all for possible bugs in the linter code. |
| 46 | defer func() { |
| 47 | if r := recover(); r != nil { |
| 48 | if err, ok := r.(error); ok { |
| 49 | pass.Reportf(n.Pos(), "internal linter error: %v", err) |
| 50 | return |
| 51 | } |
| 52 | panic(r) |
| 53 | } |
| 54 | }() |
| 55 | |
| 56 | if cmp, ok := n.(*ast.BinaryExpr); ok { |
| 57 | checkErrCmp(pass, cmp) |
| 58 | return |
| 59 | } |
| 60 | if cmp, ok := n.(*ast.TypeAssertExpr); ok { |
| 61 | checkErrCast(pass, cmp) |
| 62 | return |
| 63 | } |
| 64 | if cmp, ok := n.(*ast.SwitchStmt); ok { |
| 65 | checkErrSwitch(pass, cmp) |
| 66 | return |
| 67 | } |
| 68 | }) |
| 69 | |
| 70 | return nil, nil |
| 71 | } |
| 72 | |
| 73 | func checkErrSwitch(pass *analysis.Pass, s *ast.SwitchStmt) { |
| 74 | if pass.TypesInfo.Types[s.Tag].Type == errorType { |
| 75 | pass.Reportf(s.Switch, escNl(`invalid direct comparison of error object |
| 76 | Tip: |
| 77 | switch err { case errRef:... |
| 78 | -> switch { case errors.Is(err, errRef): ... |
| 79 | `)) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | func checkErrCast(pass *analysis.Pass, texpr *ast.TypeAssertExpr) { |
| 84 | if pass.TypesInfo.Types[texpr.X].Type == errorType { |
| 85 | pass.Reportf(texpr.Lparen, escNl(`invalid direct cast on error object |
| 86 | Alternatives: |
| 87 | if _, ok := err.(*T); ok -> if errors.HasType(err, (*T)(nil)) |
| 88 | if _, ok := err.(I); ok -> if errors.HasInterface(err, (*I)(nil)) |
| 89 | if myErr, ok := err.(*T); ok -> if myErr := (*T)(nil); errors.As(err, &myErr) |
| 90 | if myErr, ok := err.(I); ok -> if myErr := (I)(nil); errors.As(err, &myErr) |
| 91 | switch err.(type) { case *T:... -> switch { case errors.HasType(err, (*T)(nil)): ... |
| 92 | `)) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | func isEOFError(e ast.Expr) bool { |
| 97 | if s, ok := e.(*ast.SelectorExpr); ok { |
| 98 | if io, ok := s.X.(*ast.Ident); ok && io.Name == "io" && io.Obj == (*ast.Object)(nil) { |
| 99 | if s.Sel.Name == "EOF" || s.Sel.Name == "ErrUnexpectedEOF" { |
| 100 | return true |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | return false |
| 105 | } |
| 106 | |
| 107 | func checkErrCmp(pass *analysis.Pass, binaryExpr *ast.BinaryExpr) { |
| 108 | switch binaryExpr.Op { |
| 109 | case token.NEQ, token.EQL: |
| 110 | if pass.TypesInfo.Types[binaryExpr.X].Type == errorType && |
| 111 | !pass.TypesInfo.Types[binaryExpr.Y].IsNil() { |
| 112 | // We have a special case: when the RHS is io.EOF or io.ErrUnexpectedEOF. |
| 113 | // They are nearly always used with APIs that return |
| 114 | // an undecorated error. |
| 115 | if isEOFError(binaryExpr.Y) { |
| 116 | return |
| 117 | } |
| 118 | |
| 119 | pass.Reportf(binaryExpr.OpPos, escNl(`use errors.Is instead of a direct comparison |
| 120 | For example: |
| 121 | if errors.Is(err, errMyOwnErrReference) { |
| 122 | ... |
| 123 | } |
| 124 | `)) |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | func escNl(msg string) string { |
| 130 | return strings.ReplaceAll(msg, "\n", "\\n++") |
| 131 | } |