Skip to content

Commit 3c20e3f

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/analysis/yield: analyzer for iterator (yield) mistakes
This analyzer detects failure to check the result of a call to yield (which can cause a range loop to run beyond the sequence, leading to a panic). It is not always a mistake to ignore the result of a call to yield; it depends on whether control can reach another call to yield without checking that the first call returned true. Consequently, this analyzer uses SSA for control flow analysis. We plan to add this analyzer to gopls before we promote it to vet. + test, relnote Fixes golang/go#65795 Change-Id: I75fa272e2f546be0c2acb10a1978c82bc19db5bd Reviewed-on: https://go-review.googlesource.com/c/tools/+/609617 Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 221e94d commit 3c20e3f

File tree

9 files changed

+356
-1
lines changed

9 files changed

+356
-1
lines changed

gopls/doc/analyzers.md

+35
Original file line numberDiff line numberDiff line change
@@ -996,4 +996,39 @@ Default: off. Enable by setting `"analyses": {"useany": true}`.
996996

997997
Package documentation: [useany](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany)
998998

999+
<a id='yield'></a>
1000+
## `yield`: report calls to yield where the result is ignored
1001+
1002+
1003+
After a yield function returns false, the caller should not call
1004+
the yield function again; generally the iterator should return
1005+
promptly.
1006+
1007+
This example fails to check the result of the call to yield,
1008+
causing this analyzer to report a diagnostic:
1009+
1010+
yield(1) // yield may be called again (on L2) after returning false
1011+
yield(2)
1012+
1013+
The corrected code is either this:
1014+
1015+
if yield(1) { yield(2) }
1016+
1017+
or simply:
1018+
1019+
_ = yield(1) && yield(2)
1020+
1021+
It is not always a mistake to ignore the result of yield.
1022+
For example, this is a valid single-element iterator:
1023+
1024+
yield(1) // ok to ignore result
1025+
return
1026+
1027+
It is only a mistake when the yield call that returned false may be
1028+
followed by another call.
1029+
1030+
Default: on.
1031+
1032+
Package documentation: [yield](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/yield)
1033+
9991034
<!-- END Analyzers: DO NOT MANUALLY EDIT THIS SECTION -->

gopls/doc/release/v0.17.0.md

+6
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,9 @@ Gopls now offers a new code action, “Declare missing method of T.f”,
7878
where T is the concrete type and f is the undefined method.
7979
The stub method's signature is inferred
8080
from the context of the call.
81+
82+
## `yield` analyzer
83+
84+
The new `yield` analyzer detects mistakes using the `yield` function
85+
in a Go 1.23 iterator, such as failure to check its boolean result and
86+
break out of a loop.

gopls/internal/analysis/yield/doc.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2024 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 yield defines an Analyzer that checks for mistakes related
6+
// to the yield function used in iterators.
7+
//
8+
// # Analyzer yield
9+
//
10+
// yield: report calls to yield where the result is ignored
11+
//
12+
// After a yield function returns false, the caller should not call
13+
// the yield function again; generally the iterator should return
14+
// promptly.
15+
//
16+
// This example fails to check the result of the call to yield,
17+
// causing this analyzer to report a diagnostic:
18+
//
19+
// yield(1) // yield may be called again (on L2) after returning false
20+
// yield(2)
21+
//
22+
// The corrected code is either this:
23+
//
24+
// if yield(1) { yield(2) }
25+
//
26+
// or simply:
27+
//
28+
// _ = yield(1) && yield(2)
29+
//
30+
// It is not always a mistake to ignore the result of yield.
31+
// For example, this is a valid single-element iterator:
32+
//
33+
// yield(1) // ok to ignore result
34+
// return
35+
//
36+
// It is only a mistake when the yield call that returned false may be
37+
// followed by another call.
38+
package yield

gopls/internal/analysis/yield/main.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2024 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+
//go:build ignore
6+
7+
// The yield command applies the yield analyzer to the specified
8+
// packages of Go source code.
9+
package main
10+
11+
import (
12+
"golang.org/x/tools/go/analysis/singlechecker"
13+
"golang.org/x/tools/gopls/internal/analysis/yield"
14+
)
15+
16+
func main() { singlechecker.Main(yield.Analyzer) }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2024 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 yield
6+
7+
import (
8+
"bufio"
9+
"io"
10+
)
11+
12+
//
13+
//
14+
// Modify this block of comment lines as needed when changing imports
15+
// to avoid perturbing subsequent line numbers (and thus error messages).
16+
//
17+
// This is L16.
18+
19+
func goodIter(yield func(int) bool) {
20+
_ = yield(1) && yield(2) && yield(3) // ok
21+
}
22+
23+
func badIterOR(yield func(int) bool) {
24+
_ = yield(1) || // want `yield may be called again \(on L25\) after returning false`
25+
yield(2) || // want `yield may be called again \(on L26\) after returning false`
26+
yield(3)
27+
}
28+
29+
func badIterSeq(yield func(int) bool) {
30+
yield(1) // want `yield may be called again \(on L31\) after returning false`
31+
yield(2) // want `yield may be called again \(on L32\) after returning false`
32+
yield(3) // ok
33+
}
34+
35+
func badIterLoop(yield func(int) bool) {
36+
for {
37+
yield(1) // want `yield may be called again after returning false`
38+
}
39+
}
40+
41+
func goodIterLoop(yield func(int) bool) {
42+
for {
43+
if !yield(1) {
44+
break
45+
}
46+
}
47+
}
48+
49+
func badIterIf(yield func(int) bool) {
50+
ok := yield(1) // want `yield may be called again \(on L52\) after returning false`
51+
if !ok {
52+
yield(2)
53+
} else {
54+
yield(3)
55+
}
56+
}
57+
58+
func singletonIter(yield func(int) bool) {
59+
yield(1) // ok
60+
}
61+
62+
func twoArgumentYield(yield func(int, int) bool) {
63+
_ = yield(1, 1) || // want `yield may be called again \(on L64\) after returning false`
64+
yield(2, 2)
65+
}
66+
67+
func zeroArgumentYield(yield func() bool) {
68+
_ = yield() || // want `yield may be called again \(on L69\) after returning false`
69+
yield()
70+
}
71+
72+
func tricky(in io.ReadCloser) func(yield func(string, error) bool) {
73+
return func(yield func(string, error) bool) {
74+
scan := bufio.NewScanner(in)
75+
for scan.Scan() {
76+
if !yield(scan.Text(), nil) { // want `yield may be called again \(on L82\) after returning false`
77+
_ = in.Close()
78+
break
79+
}
80+
}
81+
if err := scan.Err(); err != nil {
82+
yield("", err)
83+
}
84+
}
85+
}
+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2024 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 yield
6+
7+
// TODO(adonovan): also check for this pattern:
8+
//
9+
// for x := range seq {
10+
// yield(x)
11+
// }
12+
//
13+
// which should be entirely rewritten as
14+
//
15+
// seq(yield)
16+
//
17+
// to avoid unnecesary range desugaring and chains of dynamic calls.
18+
19+
import (
20+
_ "embed"
21+
"fmt"
22+
"go/ast"
23+
"go/token"
24+
"go/types"
25+
26+
"golang.org/x/tools/go/analysis"
27+
"golang.org/x/tools/go/analysis/passes/buildssa"
28+
"golang.org/x/tools/go/analysis/passes/inspect"
29+
"golang.org/x/tools/go/ast/inspector"
30+
"golang.org/x/tools/go/ssa"
31+
"golang.org/x/tools/gopls/internal/util/safetoken"
32+
"golang.org/x/tools/internal/analysisinternal"
33+
)
34+
35+
//go:embed doc.go
36+
var doc string
37+
38+
var Analyzer = &analysis.Analyzer{
39+
Name: "yield",
40+
Doc: analysisinternal.MustExtractDoc(doc, "yield"),
41+
Requires: []*analysis.Analyzer{inspect.Analyzer, buildssa.Analyzer},
42+
Run: run,
43+
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/yield",
44+
}
45+
46+
func run(pass *analysis.Pass) (interface{}, error) {
47+
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
48+
49+
// Find all calls to yield of the right type.
50+
yieldCalls := make(map[token.Pos]*ast.CallExpr) // keyed by CallExpr.Lparen.
51+
nodeFilter := []ast.Node{(*ast.CallExpr)(nil)}
52+
inspector.Preorder(nodeFilter, func(n ast.Node) {
53+
call := n.(*ast.CallExpr)
54+
if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "yield" {
55+
if sig, ok := pass.TypesInfo.TypeOf(id).(*types.Signature); ok &&
56+
sig.Params().Len() < 3 &&
57+
sig.Results().Len() == 1 &&
58+
types.Identical(sig.Results().At(0).Type(), types.Typ[types.Bool]) {
59+
yieldCalls[call.Lparen] = call
60+
}
61+
}
62+
})
63+
64+
// Common case: nothing to do.
65+
if len(yieldCalls) == 0 {
66+
return nil, nil
67+
}
68+
69+
// Study the control flow using SSA.
70+
buildssa := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
71+
for _, fn := range buildssa.SrcFuncs {
72+
// TODO(adonovan): opt: skip functions that don't contain any yield calls.
73+
74+
// Find the yield calls in SSA.
75+
type callInfo struct {
76+
syntax *ast.CallExpr
77+
index int // index of instruction within its block
78+
reported bool
79+
}
80+
ssaYieldCalls := make(map[*ssa.Call]*callInfo)
81+
for _, b := range fn.Blocks {
82+
for i, instr := range b.Instrs {
83+
if call, ok := instr.(*ssa.Call); ok {
84+
if syntax, ok := yieldCalls[call.Pos()]; ok {
85+
ssaYieldCalls[call] = &callInfo{syntax: syntax, index: i}
86+
}
87+
}
88+
}
89+
}
90+
91+
// Now search for a control path from the instruction after a
92+
// yield call to another yield call--possible the same one,
93+
// following all block successors except "if yield() { ... }";
94+
// in such cases we know that yield returned true.
95+
for call, info := range ssaYieldCalls {
96+
visited := make([]bool, len(fn.Blocks)) // visited BasicBlock.Indexes
97+
98+
// visit visits the instructions of a block (or a suffix if start > 0).
99+
var visit func(b *ssa.BasicBlock, start int)
100+
visit = func(b *ssa.BasicBlock, start int) {
101+
if !visited[b.Index] {
102+
if start == 0 {
103+
visited[b.Index] = true
104+
}
105+
for _, instr := range b.Instrs[start:] {
106+
switch instr := instr.(type) {
107+
case *ssa.Call:
108+
if !info.reported && ssaYieldCalls[instr] != nil {
109+
info.reported = true
110+
where := "" // "" => same yield call (a loop)
111+
if instr != call {
112+
otherLine := safetoken.StartPosition(pass.Fset, instr.Pos()).Line
113+
where = fmt.Sprintf("(on L%d) ", otherLine)
114+
}
115+
pass.Reportf(call.Pos(), "yield may be called again %safter returning false", where)
116+
}
117+
case *ssa.If:
118+
// Visit both successors, unless cond is yield() or its negation.
119+
// In that case visit only the "if !yield()" block.
120+
cond := instr.Cond
121+
t, f := b.Succs[0], b.Succs[1]
122+
if unop, ok := cond.(*ssa.UnOp); ok && unop.Op == token.NOT {
123+
cond, t, f = unop.X, f, t
124+
}
125+
if cond, ok := cond.(*ssa.Call); ok && ssaYieldCalls[cond] != nil {
126+
// Skip the successor reached by "if yield() { ... }".
127+
} else {
128+
visit(t, 0)
129+
}
130+
visit(f, 0)
131+
132+
case *ssa.Jump:
133+
visit(b.Succs[0], 0)
134+
}
135+
}
136+
}
137+
}
138+
139+
// Start at the instruction after the yield call.
140+
visit(call.Block(), info.index+1)
141+
}
142+
}
143+
144+
return nil, nil
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 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 yield_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/gopls/internal/analysis/yield"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testdata := analysistest.TestData()
16+
analysistest.Run(t, testdata, yield.Analyzer, "a")
17+
}

gopls/internal/doc/api.json

+11
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,11 @@
613613
"Name": "\"useany\"",
614614
"Doc": "check for constraints that could be simplified to \"any\"",
615615
"Default": "false"
616+
},
617+
{
618+
"Name": "\"yield\"",
619+
"Doc": "report calls to yield where the result is ignored\n\nAfter a yield function returns false, the caller should not call\nthe yield function again; generally the iterator should return\npromptly.\n\nThis example fails to check the result of the call to yield,\ncausing this analyzer to report a diagnostic:\n\n\tyield(1) // yield may be called again (on L2) after returning false\n\tyield(2)\n\nThe corrected code is either this:\n\n\tif yield(1) { yield(2) }\n\nor simply:\n\n\t_ = yield(1) \u0026\u0026 yield(2)\n\nIt is not always a mistake to ignore the result of yield.\nFor example, this is a valid single-element iterator:\n\n\tyield(1) // ok to ignore result\n\treturn\n\nIt is only a mistake when the yield call that returned false may be\nfollowed by another call.",
620+
"Default": "true"
616621
}
617622
]
618623
},
@@ -1303,6 +1308,12 @@
13031308
"Doc": "check for constraints that could be simplified to \"any\"",
13041309
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany",
13051310
"Default": false
1311+
},
1312+
{
1313+
"Name": "yield",
1314+
"Doc": "report calls to yield where the result is ignored\n\nAfter a yield function returns false, the caller should not call\nthe yield function again; generally the iterator should return\npromptly.\n\nThis example fails to check the result of the call to yield,\ncausing this analyzer to report a diagnostic:\n\n\tyield(1) // yield may be called again (on L2) after returning false\n\tyield(2)\n\nThe corrected code is either this:\n\n\tif yield(1) { yield(2) }\n\nor simply:\n\n\t_ = yield(1) \u0026\u0026 yield(2)\n\nIt is not always a mistake to ignore the result of yield.\nFor example, this is a valid single-element iterator:\n\n\tyield(1) // ok to ignore result\n\treturn\n\nIt is only a mistake when the yield call that returned false may be\nfollowed by another call.",
1315+
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/yield",
1316+
"Default": true
13061317
}
13071318
],
13081319
"Hints": [

0 commit comments

Comments
 (0)