Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/sqlrowserr/sqlrowserr.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package sqlrowserr defines an analyzer for uses of sql.Rows
     6  // in which the user has forgotten to check Rows.Err.
     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  // TODO(adonovan): factor common structures with the scannererr analyzer.
    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  			// var sc, err = db.Query(...)
    83  			curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, 0)
    84  			lhs = curName.Node().(*ast.Ident)
    85  		case edge.AssignStmt_Rhs:
    86  			// sc, err := db.Query(...)   (or '=')
    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  		// Have: rows, err := db.Query(...)
    99  
   100  		// Check all uses of the var rows.
   101  		nextLoop := token.NoPos // position of rows.Next() call within a loop
   102  		for curUse := range index.Uses(rows) {
   103  			// If the var rows is used in a context other than rows.Method(...),
   104  			// assume conservatively that it may escape, and reject this candidate.
   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  				// If the rows.Err method is called anywhere, reject this candidate.
   113  				return
   114  			case "Next":
   115  				// The Next call must be in a loop that intervenes the declaration of rows.
   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  	// Check each query method in the sql package that returns (*Rows, error).
   135  	// (This could be generalized for arbitrary such functions...)
   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