| // Copyright 2026 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. | |
| // Package sqlrowserr defines an analyzer for uses of sql.Rows | |
| // in which the user has forgotten to check Rows.Err. | |
| package sqlrowserr | |
| 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/edge" | |
| "golang.org/x/tools/go/ast/inspector" | |
| typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" | |
| "golang.org/x/tools/internal/moreiters" | |
| "golang.org/x/tools/internal/typesinternal/typeindex" | |
| ) | |
| const doc = `sqlrowserr: report failure to check sql.Rows.Err | |
| This analyzer reports uses of sql.Rows in which the result of a query | |
| such as db.Query() is assigned to a local variable that is then used | |
| in a loop that calls Rows.Next, but lacks a final check of Rows.Err. | |
| This causes row iteration errors to be discarded. | |
| For example: | |
| rows, err := db.Query("select ...") // error: "sql.Rows rows is used in Next loop without final check of rows.Err()" | |
| if err != nil { | |
| return err | |
| } | |
| defer rows.Close() // ignore error | |
| for rows.Next() { | |
| var x int | |
| if err := rows.Scan(&x); err != nil { | |
| return err | |
| } | |
| use(x) | |
| } | |
| /* ...no use of rows.Err()... */ | |
| Correct usage of sql.Rows demands both a call to Rows.Close to release | |
| resources and a call to Rows.Err to report iteration errors. It is | |
| not critical to report resource cleanup errors, but it is crucial to | |
| report iteration errors as they would otherwise be indistinguishable | |
| from a smaller result. | |
| To avoid false positives, the analyzer is silent if the Rows is passed | |
| into or out of the function or assigned somewhere other than a local | |
| variable. | |
| It is not this analyzer's goal to ensure proper handling of errors in | |
| all cases, but merely the simple mistakes where the user may have been | |
| oblivious to the existence of the Rows.Err method. | |
| ` | |
| var Analyzer = &analysis.Analyzer{ | |
| Name: "sqlrowserr", | |
| Doc: doc, | |
| URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sqlrowserr", | |
| Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer}, | |
| Run: run, | |
| } | |
| // TODO(adonovan): factor common structures with the scannererr analyzer. | |
| func run(pass *analysis.Pass) (any, error) { | |
| var ( | |
| index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) | |
| info = pass.TypesInfo | |
| ) | |
| checkCall := func(curCall inspector.Cursor) { | |
| var lhs ast.Expr | |
| switch curCall.ParentEdgeKind() { | |
| case edge.ValueSpec_Values: | |
| // var sc, err = db.Query(...) | |
| curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, 0) | |
| lhs = curName.Node().(*ast.Ident) | |
| case edge.AssignStmt_Rhs: | |
| // sc, err := db.Query(...) (or '=') | |
| curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, 0) | |
| lhs = curLhs.Node().(ast.Expr) | |
| } | |
| id, ok := lhs.(*ast.Ident) | |
| if !ok { | |
| return | |
| } | |
| rows, ok := info.ObjectOf(id).(*types.Var) | |
| if !ok { | |
| return | |
| } | |
| // Have: rows, err := db.Query(...) | |
| // Check all uses of the var rows. | |
| nextLoop := token.NoPos // position of rows.Next() call within a loop | |
| for curUse := range index.Uses(rows) { | |
| // If the var rows is used in a context other than rows.Method(...), | |
| // assume conservatively that it may escape, and reject this candidate. | |
| if curUse.ParentEdgeKind() != edge.SelectorExpr_X || | |
| curUse.Parent().ParentEdgeKind() != edge.CallExpr_Fun { | |
| return | |
| } | |
| switch curUse.Parent().Node().(*ast.SelectorExpr).Sel.Name { | |
| case "Err": | |
| // If the rows.Err method is called anywhere, reject this candidate. | |
| return | |
| case "Next": | |
| // The Next call must be in a loop that intervenes the declaration of rows. | |
| if curLoop, ok := moreiters.First(curUse.Enclosing((*ast.RangeStmt)(nil), (*ast.ForStmt)(nil))); ok { | |
| if curLoop.Node().Pos() > rows.Pos() { | |
| nextLoop = curUse.Node().Pos() | |
| } | |
| } | |
| } | |
| } | |
| if !nextLoop.IsValid() { | |
| return | |
| } | |
| pass.Report(analysis.Diagnostic{ | |
| Pos: curCall.Node().Pos(), | |
| End: curCall.Node().End(), | |
| Message: fmt.Sprintf("sql.Rows %q is used in Next loop at line %d without final check of %s.Err()", | |
| rows.Name(), pass.Fset.Position(nextLoop).Line, rows.Name()), | |
| }) | |
| } | |
| // Check each query method in the sql package that returns (*Rows, error). | |
| // (This could be generalized for arbitrary such functions...) | |
| for _, m := range [...][2]string{ | |
| {"Conn", "QueryContext"}, | |
| {"DB", "QueryContext"}, | |
| {"DB", "Query"}, | |
| {"Stmt", "QueryContext"}, | |
| {"Stmt", "Query"}, | |
| {"Tx", "QueryContext"}, | |
| {"Tx", "Query"}, | |
| } { | |
| for cur := range index.Calls(index.Selection("database/sql", m[0], m[1])) { | |
| checkCall(cur) | |
| } | |
| } | |
| return nil, nil | |
| } |