Skip to content

Commit e8e1fa9

Browse files
committed
go/analysis/passes/loopclosure: restore exact Go 1.20 logic, with future logic now only accessible via internal flag
1 parent 2a84b3d commit e8e1fa9

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2022 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+
// The loopclosure command applies the golang.org/x/tools/go/analysis/passes/loopclosure
6+
// analysis to the specified packages of Go source code.
7+
// It enables additional experimental checking that is disabled by default in Go 1.20.
8+
//
9+
package main
10+
11+
import (
12+
"golang.org/x/tools/go/analysis/passes/loopclosure"
13+
"golang.org/x/tools/go/analysis/singlechecker"
14+
"golang.org/x/tools/internal/analysisinternal"
15+
)
16+
17+
func main() {
18+
analysisinternal.LoopclosureGo121 = true
19+
singlechecker.Main(loopclosure.Analyzer)
20+
}

go/analysis/passes/loopclosure/loopclosure.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"golang.org/x/tools/go/analysis/passes/inspect"
1919
"golang.org/x/tools/go/ast/inspector"
2020
"golang.org/x/tools/go/types/typeutil"
21+
"golang.org/x/tools/internal/analysisinternal"
2122
)
2223

2324
const Doc = `check references to loop variables from within nested functions
@@ -87,6 +88,108 @@ var Analyzer = &analysis.Analyzer{
8788
}
8889

8990
func run(pass *analysis.Pass) (interface{}, error) {
91+
// Check if we are enabling additional experimental logic.
92+
if !analysisinternal.LoopclosureGo121 {
93+
return runGo120(pass)
94+
}
95+
return runGo121(pass)
96+
}
97+
98+
// runGo120 runs the analyzer with logic intended for Go 1.20 cmd/vet.
99+
// TODO: delete this once the Go 1.21 dev cycle has started.
100+
func runGo120(pass *analysis.Pass) (interface{}, error) {
101+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
102+
103+
nodeFilter := []ast.Node{
104+
(*ast.RangeStmt)(nil),
105+
(*ast.ForStmt)(nil),
106+
}
107+
inspect.Preorder(nodeFilter, func(n ast.Node) {
108+
// Find the variables updated by the loop statement.
109+
vars := make(map[types.Object]int)
110+
addVar := func(expr ast.Expr) {
111+
if id, _ := expr.(*ast.Ident); id != nil {
112+
if obj := pass.TypesInfo.ObjectOf(id); obj != nil {
113+
// For runGo121, we use a count to track when to remove
114+
// elements from the vars map.
115+
// For runGo120, we do not, but set the proper count anyway.
116+
vars[obj] = 1
117+
}
118+
}
119+
}
120+
var body *ast.BlockStmt
121+
switch n := n.(type) {
122+
case *ast.RangeStmt:
123+
body = n.Body
124+
addVar(n.Key)
125+
addVar(n.Value)
126+
case *ast.ForStmt:
127+
body = n.Body
128+
switch post := n.Post.(type) {
129+
case *ast.AssignStmt:
130+
// e.g. for p = head; p != nil; p = p.next
131+
for _, lhs := range post.Lhs {
132+
addVar(lhs)
133+
}
134+
case *ast.IncDecStmt:
135+
// e.g. for i := 0; i < n; i++
136+
addVar(post.X)
137+
}
138+
}
139+
if vars == nil {
140+
return
141+
}
142+
143+
// Inspect statements to find function literals that may be run outside of
144+
// the current loop iteration.
145+
//
146+
// For go, defer, and errgroup.Group.Go, we ignore all but the last
147+
// statement, where "last" is defined recursively.
148+
// See runGo120 for an alternative approach.
149+
v := visitor{last: func(v visitor, last ast.Stmt) {
150+
var stmts []ast.Stmt
151+
switch s := last.(type) {
152+
case *ast.GoStmt:
153+
stmts = litStmts(s.Call.Fun)
154+
case *ast.DeferStmt:
155+
stmts = litStmts(s.Call.Fun)
156+
case *ast.ExprStmt: // check for errgroup.Group.Go
157+
if call, ok := s.X.(*ast.CallExpr); ok {
158+
stmts = litStmts(goInvoke(pass.TypesInfo, call))
159+
}
160+
}
161+
for _, stmt := range stmts {
162+
reportCaptured(pass, vars, stmt)
163+
}
164+
}}
165+
v.visit(body.List)
166+
167+
// Also check for testing.T.Run (with T.Parallel).
168+
// We consider every t.Run statement in the loop body, because there is
169+
// no commonly used mechanism for synchronizing parallel subtests.
170+
// It is of course theoretically possible to synchronize parallel subtests,
171+
// though such a pattern is likely to be exceedingly rare as it would be
172+
// fighting against the test runner.
173+
for _, s := range body.List {
174+
switch s := s.(type) {
175+
case *ast.ExprStmt:
176+
if call, ok := s.X.(*ast.CallExpr); ok {
177+
for _, stmt := range parallelSubtest(pass.TypesInfo, call) {
178+
reportCaptured(pass, vars, stmt)
179+
}
180+
}
181+
}
182+
}
183+
})
184+
return nil, nil
185+
}
186+
187+
// runGo121 runs the analyzer with additional experimental logic
188+
// that is not intended for Go 1.20 cmd/vet, including examining
189+
// statements following a go, defer or errgroup.Group.Go statement
190+
// to determine if they cannot delay start of execution of the
191+
// go or defer.
192+
func runGo121(pass *analysis.Pass) (interface{}, error) {
90193
// We do two passes with inspect: first, a simpler walk
91194
// looking for problems with testing.T.Run with T.Parallel, and
92195
// second, a more involved walk to examine last statements

go/analysis/passes/loopclosure/loopclosure_test.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,25 @@ import (
99

1010
"golang.org/x/tools/go/analysis/analysistest"
1111
"golang.org/x/tools/go/analysis/passes/loopclosure"
12+
"golang.org/x/tools/internal/analysisinternal"
1213
"golang.org/x/tools/internal/typeparams"
1314
)
1415

1516
func Test(t *testing.T) {
1617
testdata := analysistest.TestData()
17-
tests := []string{"a", "golang.org/...", "subtests", "go121"}
18+
tests := []string{"a", "golang.org/...", "subtests"}
1819
if typeparams.Enabled {
1920
tests = append(tests, "typeparams")
2021
}
2122
analysistest.Run(t, testdata, loopclosure.Analyzer, tests...)
23+
24+
// Enable experimental checking that is disabled by default in Go 1.20.
25+
defer func(go121Test bool) {
26+
analysisinternal.LoopclosureGo121 = go121Test
27+
}(analysisinternal.LoopclosureGo121)
28+
analysisinternal.LoopclosureGo121 = true
29+
30+
// Re-run everything, plus the go121 tests
31+
tests = append(tests, "go121")
32+
analysistest.Run(t, testdata, loopclosure.Analyzer, tests...)
2233
}

internal/analysisinternal/analysis.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import (
1919
// in Go 1.18+.
2020
var DiagnoseFuzzTests bool = false
2121

22+
// LoopclosureGo121 controls whether the 'loopclosure' analyzer performs additional
23+
// diagnoses that are disabled in Go 1.20.
24+
var LoopclosureGo121 bool = false
25+
2226
func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
2327
// Get the end position for the type error.
2428
offset, end := fset.PositionFor(start, false).Offset, start

0 commit comments

Comments
 (0)