1
2
3
4
5
6
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
77
78
79
80
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
88 var lhs ast.Expr
89 switch ek, idx := curCall.ParentEdge(); ek {
90 case edge.ValueSpec_Values:
91
92 curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
93 lhs = curName.Node().(*ast.Ident)
94 case edge.AssignStmt_Rhs:
95
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
108
109
110 scanLoop := token.NoPos
111 for curUse := range index.Uses(sc) {
112
113
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
122 continue callLoop
123 case "Scan":
124
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