blob: 0b5afb978ec3c22b6f72426d42730d4f08b749ab [file] [log] [blame]
Tim Windelschmidt1f4590b2025-07-29 23:05:36 +02001// 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.
6package errcmp
7
8import (
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.
20const Doc = `check for comparison of error objects`
21
22var errorType = types.Universe.Lookup("error").Type()
23
24// Analyzer checks for usage of errors.Is instead of direct ==/!=
25// comparisons.
26var Analyzer = &analysis.Analyzer{
27 Name: "errcmp",
28 Doc: Doc,
29 Requires: []*analysis.Analyzer{inspect.Analyzer},
30 Run: run,
31}
32
33func 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
73func 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
76Tip:
77 switch err { case errRef:...
78-> switch { case errors.Is(err, errRef): ...
79`))
80 }
81}
82
83func 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
86Alternatives:
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
96func 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
107func 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
120For example:
121 if errors.Is(err, errMyOwnErrReference) {
122 ...
123 }
124`))
125 }
126 }
127}
128
129func escNl(msg string) string {
130 return strings.ReplaceAll(msg, "\n", "\\n++")
131}