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

     1  // Copyright 2025 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 scannererr defines an analyzer for uses of bufio.Scanner
     6  // in which the user has forgotten to check Scanner.Err.
     7  package scannererr
     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  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    19  	"golang.org/x/tools/internal/moreiters"
    20  	"golang.org/x/tools/internal/typesinternal"
    21  	"golang.org/x/tools/internal/typesinternal/typeindex"
    22  )
    23  
    24  const doc = `scannererr: report failure to check bufio.Scanner.Err
    25  
    26  This analyzer reports uses of bufio.Scanner in which the result of
    27  NewScanner is assigned to a local variable that is then used in a loop
    28  that calls Scanner.Scan, but lacks a final check of Scanner.Err,
    29  which is how I/O errors are reported.
    30  
    31  For example:
    32  
    33  	sc := bufio.NewScanner(os.Stdin) // error: "bufio.Scanner sc is used in Scan loop without final check of sc.Err()"
    34  	for sc.Scan() {
    35  		line := sc.Text()
    36  		use(line)
    37  	}
    38  	/* ...no use of sc.Err()... */
    39  
    40  To avoid false positives, the analyzer is silent if the scanner is
    41  passed into or out of the function or assigned somewhere other than a
    42  local variable.
    43  
    44  It is not this analyzer's goal to ensure proper handling of errors in
    45  all cases, but merely the simple mistakes where the user may have been
    46  oblivious to the existence of the Scanner.Err method.
    47  
    48  The analyzer ignores calls to bufio.NewScanner whose argument is an
    49  infallible memory-backed io.Reader such as strings.Reader or bytes.Buffer.
    50  (In such cases, Scan may yet fail if a token or line is too long for the
    51  scanner's internal buffer, but this is rare.)
    52  
    53  If you know that errors are impossible for a given scanner, you can
    54  suppress the diagnostic thus:
    55  
    56  	_ = sc.Err() // ignore error; neither reading nor scanning can fail
    57  
    58  `
    59  
    60  var Analyzer = &analysis.Analyzer{
    61  	Name:     "scannererr",
    62  	Doc:      doc,
    63  	URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/scannererr",
    64  	Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer},
    65  	Run:      run,
    66  }
    67  
    68  func run(pass *analysis.Pass) (any, error) {
    69  	var (
    70  		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    71  		info  = pass.TypesInfo
    72  	)
    73  
    74  callLoop:
    75  	for curCall := range index.Calls(index.Object("bufio", "NewScanner")) {
    76  		// If NewScanner's reader operand is infallible (such
    77  		// as strings.Reader, bytes.Reader, or bytes.Buffer),
    78  		// disregard it. (Scanning can still fail if a token
    79  		// or line is longer than the internal buffer, but
    80  		// that's much less common than an I/O error).
    81  		tArg := info.TypeOf(curCall.Node().(*ast.CallExpr).Args[0])
    82  		if typesinternal.IsPointerToNamed(tArg, "bytes", "Buffer", "Reader") ||
    83  			typesinternal.IsPointerToNamed(tArg, "strings", "Reader") {
    84  			continue
    85  		}
    86  
    87  		// TODO(adonovan): factor this common pattern.
    88  		var lhs ast.Expr
    89  		switch ek, idx := curCall.ParentEdge(); ek {
    90  		case edge.ValueSpec_Values:
    91  			// var sc = bufio.NewScanner(...)
    92  			curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
    93  			lhs = curName.Node().(*ast.Ident)
    94  		case edge.AssignStmt_Rhs:
    95  			// sc := bufio.NewScanner(...)   (or '=')
    96  			curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
    97  			lhs = curLhs.Node().(ast.Expr)
    98  		}
    99  		id, ok := lhs.(*ast.Ident)
   100  		if !ok {
   101  			continue
   102  		}
   103  		sc, ok := info.ObjectOf(id).(*types.Var)
   104  		if !ok {
   105  			continue
   106  		}
   107  		// Have: sc := bufio.NewScanner(...)
   108  
   109  		// Check all uses of the var sc.
   110  		scanLoop := token.NoPos // position of sc.Scan() call within a loop
   111  		for curUse := range index.Uses(sc) {
   112  			// If the var sc is used in a context other than sc.Method(...),
   113  			// assume conservatively that it may escape, and reject this candidate.
   114  			if curUse.ParentEdgeKind() != edge.SelectorExpr_X ||
   115  				curUse.Parent().ParentEdgeKind() != edge.CallExpr_Fun {
   116  				continue callLoop
   117  			}
   118  
   119  			switch curUse.Parent().Node().(*ast.SelectorExpr).Sel.Name {
   120  			case "Err":
   121  				// If the sc.Err method is called anywhere, reject this candidate.
   122  				continue callLoop
   123  			case "Scan":
   124  				// The Scan call must be in a loop that intervenes the declaration of sc.
   125  				if curLoop, ok := moreiters.First(curUse.Enclosing((*ast.RangeStmt)(nil), (*ast.ForStmt)(nil))); ok {
   126  					if curLoop.Node().Pos() > sc.Pos() {
   127  						scanLoop = curUse.Node().Pos()
   128  					}
   129  				}
   130  			}
   131  		}
   132  		if !scanLoop.IsValid() {
   133  			continue
   134  		}
   135  
   136  		pass.Report(analysis.Diagnostic{
   137  			Pos: curCall.Node().Pos(),
   138  			End: curCall.Node().End(),
   139  			Message: fmt.Sprintf("bufio.Scanner %q is used in Scan loop at line %d without final check of %s.Err()",
   140  				sc.Name(), pass.Fset.Position(scanLoop).Line, sc.Name()),
   141  		})
   142  	}
   143  
   144  	return nil, nil
   145  }
   146  

View as plain text