1
2
3
4
5
6
7 package sqlrowserr
8
9 import (
10 "fmt"
11 "go/ast"
12 "go/token"
13 "go/types"
14
15 "golang.org/x/tools/go/analysis"
16 "golang.org/x/tools/go/analysis/passes/inspect"
17 "golang.org/x/tools/go/ast/edge"
18 "golang.org/x/tools/go/ast/inspector"
19 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
20 "golang.org/x/tools/internal/moreiters"
21 "golang.org/x/tools/internal/typesinternal/typeindex"
22 )
23
24 const doc = `sqlrowserr: report failure to check sql.Rows.Err
25
26 This analyzer reports uses of sql.Rows in which the result of a query
27 such as db.Query() is assigned to a local variable that is then used
28 in a loop that calls Rows.Next, but lacks a final check of Rows.Err.
29 This causes row iteration errors to be discarded.
30
31 For example:
32
33 rows, err := db.Query("select ...") // error: "sql.Rows rows is used in Next loop without final check of rows.Err()"
34 if err != nil {
35 return err
36 }
37 defer rows.Close() // ignore error
38 for rows.Next() {
39 var x int
40 if err := rows.Scan(&x); err != nil {
41 return err
42 }
43 use(x)
44 }
45 /* ...no use of rows.Err()... */
46
47 Correct usage of sql.Rows demands both a call to Rows.Close to release
48 resources and a call to Rows.Err to report iteration errors. It is
49 not critical to report resource cleanup errors, but it is crucial to
50 report iteration errors as they would otherwise be indistinguishable
51 from a smaller result.
52
53 To avoid false positives, the analyzer is silent if the Rows is passed
54 into or out of the function or assigned somewhere other than a local
55 variable.
56
57 It is not this analyzer's goal to ensure proper handling of errors in
58 all cases, but merely the simple mistakes where the user may have been
59 oblivious to the existence of the Rows.Err method.
60 `
61
62 var Analyzer = &analysis.Analyzer{
63 Name: "sqlrowserr",
64 Doc: doc,
65 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sqlrowserr",
66 Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer},
67 Run: run,
68 }
69
70
71
72 func run(pass *analysis.Pass) (any, error) {
73 var (
74 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
75 info = pass.TypesInfo
76 )
77
78 checkCall := func(curCall inspector.Cursor) {
79 var lhs ast.Expr
80 switch curCall.ParentEdgeKind() {
81 case edge.ValueSpec_Values:
82
83 curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, 0)
84 lhs = curName.Node().(*ast.Ident)
85 case edge.AssignStmt_Rhs:
86
87 curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, 0)
88 lhs = curLhs.Node().(ast.Expr)
89 }
90 id, ok := lhs.(*ast.Ident)
91 if !ok {
92 return
93 }
94 rows, ok := info.ObjectOf(id).(*types.Var)
95 if !ok {
96 return
97 }
98
99
100
101 nextLoop := token.NoPos
102 for curUse := range index.Uses(rows) {
103
104
105 if curUse.ParentEdgeKind() != edge.SelectorExpr_X ||
106 curUse.Parent().ParentEdgeKind() != edge.CallExpr_Fun {
107 return
108 }
109
110 switch curUse.Parent().Node().(*ast.SelectorExpr).Sel.Name {
111 case "Err":
112
113 return
114 case "Next":
115
116 if curLoop, ok := moreiters.First(curUse.Enclosing((*ast.RangeStmt)(nil), (*ast.ForStmt)(nil))); ok {
117 if curLoop.Node().Pos() > rows.Pos() {
118 nextLoop = curUse.Node().Pos()
119 }
120 }
121 }
122 }
123 if !nextLoop.IsValid() {
124 return
125 }
126 pass.Report(analysis.Diagnostic{
127 Pos: curCall.Node().Pos(),
128 End: curCall.Node().End(),
129 Message: fmt.Sprintf("sql.Rows %q is used in Next loop at line %d without final check of %s.Err()",
130 rows.Name(), pass.Fset.Position(nextLoop).Line, rows.Name()),
131 })
132 }
133
134
135
136 for _, m := range [...][2]string{
137 {"Conn", "QueryContext"},
138 {"DB", "QueryContext"},
139 {"DB", "Query"},
140 {"Stmt", "QueryContext"},
141 {"Stmt", "Query"},
142 {"Tx", "QueryContext"},
143 {"Tx", "Query"},
144 } {
145 for cur := range index.Calls(index.Selection("database/sql", m[0], m[1])) {
146 checkCall(cur)
147 }
148 }
149
150 return nil, nil
151 }
152
View as plain text