Skip to content

Commit cd62b43

Browse files
committed
go/analysis/passes/loopclosure: disable checker after go1.22.
Uses the (*types.Info).FileVersion to disable the loopclosure checker when in an *ast.File that uses GoVersion >= 1.22. Updates golang/go#62605 Updates golang/go#63888 Change-Id: I2ebe974bc2ee2323eafb0f02d455ab76b3b9268d Reviewed-on: https://go-review.googlesource.com/c/tools/+/539016 Run-TryBot: Tim King <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent a1ca4fc commit cd62b43

File tree

7 files changed

+167
-10
lines changed

7 files changed

+167
-10
lines changed

go/analysis/passes/loopclosure/doc.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
// in such a way (e.g. with go or defer) that it may outlive the loop
1515
// iteration and possibly observe the wrong value of the variable.
1616
//
17+
// Note: An iteration variable can only outlive a loop iteration in Go versions <=1.21.
18+
// In Go 1.22 and later, the loop variable lifetimes changed to create a new
19+
// iteration variable per loop iteration. (See go.dev/issue/60078.)
20+
//
1721
// In this example, all the deferred functions run after the loop has
18-
// completed, so all observe the final value of v.
22+
// completed, so all observe the final value of v [<go1.22].
1923
//
2024
// for _, v := range list {
2125
// defer func() {
@@ -32,7 +36,10 @@
3236
// }()
3337
// }
3438
//
35-
// The next example uses a go statement and has a similar problem.
39+
// After Go version 1.22, the previous two for loops are equivalent
40+
// and both are correct.
41+
//
42+
// The next example uses a go statement and has a similar problem [<go1.22].
3643
// In addition, it has a data race because the loop updates v
3744
// concurrent with the goroutines accessing it.
3845
//
@@ -56,7 +63,7 @@
5663
// }
5764
//
5865
// The t.Parallel() call causes the rest of the function to execute
59-
// concurrent with the loop.
66+
// concurrent with the loop [<go1.22].
6067
//
6168
// The analyzer reports references only in the last statement,
6269
// as it is not deep enough to understand the effects of subsequent

go/analysis/passes/loopclosure/loopclosure.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
1515
"golang.org/x/tools/go/ast/inspector"
1616
"golang.org/x/tools/go/types/typeutil"
17+
"golang.org/x/tools/internal/versions"
1718
)
1819

1920
//go:embed doc.go
@@ -31,10 +32,15 @@ func run(pass *analysis.Pass) (interface{}, error) {
3132
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
3233

3334
nodeFilter := []ast.Node{
35+
(*ast.File)(nil),
3436
(*ast.RangeStmt)(nil),
3537
(*ast.ForStmt)(nil),
3638
}
37-
inspect.Preorder(nodeFilter, func(n ast.Node) {
39+
inspect.Nodes(nodeFilter, func(n ast.Node, push bool) bool {
40+
if !push {
41+
// inspect.Nodes is slightly suboptimal as we only use push=true.
42+
return true
43+
}
3844
// Find the variables updated by the loop statement.
3945
var vars []types.Object
4046
addVar := func(expr ast.Expr) {
@@ -46,6 +52,11 @@ func run(pass *analysis.Pass) (interface{}, error) {
4652
}
4753
var body *ast.BlockStmt
4854
switch n := n.(type) {
55+
case *ast.File:
56+
// Only traverse the file if its goversion is strictly before go1.22.
57+
goversion := versions.Lang(versions.FileVersions(pass.TypesInfo, n))
58+
// goversion is empty for older go versions (or the version is invalid).
59+
return goversion == "" || versions.Compare(goversion, "go1.22") < 0
4960
case *ast.RangeStmt:
5061
body = n.Body
5162
addVar(n.Key)
@@ -64,7 +75,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
6475
}
6576
}
6677
if vars == nil {
67-
return
78+
return true
6879
}
6980

7081
// Inspect statements to find function literals that may be run outside of
@@ -113,6 +124,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
113124
}
114125
}
115126
}
127+
return true
116128
})
117129
return nil, nil
118130
}

go/analysis/passes/loopclosure/loopclosure_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
package loopclosure_test
66

77
import (
8+
"os"
9+
"path/filepath"
810
"testing"
911

12+
"golang.org/x/tools/go/analysis"
1013
"golang.org/x/tools/go/analysis/analysistest"
1114
"golang.org/x/tools/go/analysis/passes/loopclosure"
15+
"golang.org/x/tools/internal/testenv"
1216
"golang.org/x/tools/internal/typeparams"
17+
"golang.org/x/tools/txtar"
1318
)
1419

1520
func Test(t *testing.T) {
@@ -20,3 +25,43 @@ func Test(t *testing.T) {
2025
}
2126
analysistest.Run(t, testdata, loopclosure.Analyzer, tests...)
2227
}
28+
29+
func TestVersions22(t *testing.T) {
30+
testenv.NeedsGo1Point(t, 22)
31+
32+
testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go22.txtar")
33+
runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions")
34+
}
35+
36+
func TestVersions18(t *testing.T) {
37+
testenv.NeedsGo1Point(t, 18)
38+
39+
testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar")
40+
runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions")
41+
}
42+
43+
// runTxtarFile unpacks a txtar archive to a directory, and runs
44+
// analyzer on the given patterns.
45+
//
46+
// This is compatible with a go.mod file.
47+
//
48+
// TODO(taking): Consider unifying with analysistest.
49+
func runTxtarFile(t *testing.T, path string, analyzer *analysis.Analyzer, patterns ...string) {
50+
ar, err := txtar.ParseFile(path)
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
55+
dir := t.TempDir()
56+
for _, file := range ar.Files {
57+
name, content := file.Name, file.Data
58+
59+
filename := filepath.Join(dir, name)
60+
os.MkdirAll(filepath.Dir(filename), 0777) // ignore error
61+
if err := os.WriteFile(filename, content, 0666); err != nil {
62+
t.Fatal(err)
63+
}
64+
}
65+
66+
analysistest.Run(t, dir, analyzer, patterns...)
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Test loopclosure at go version go1.18.
2+
3+
-- go.mod --
4+
module golang.org/fake/versions
5+
6+
go 1.18
7+
-- pre.go --
8+
//go:build go1.18
9+
10+
package versions
11+
12+
func InGo18(l []int) {
13+
for i, v := range l {
14+
go func() {
15+
print(i) // want "loop variable i captured by func literal"
16+
print(v) // want "loop variable v captured by func literal"
17+
}()
18+
}
19+
}
20+
-- go22.go --
21+
//go:build go1.22
22+
23+
package versions
24+
25+
func InGo22(l []int) {
26+
for i, v := range l {
27+
go func() {
28+
print(i) // Not reported due to file's GoVersion.
29+
print(v) // Not reported due to file's GoVersion.
30+
}()
31+
}
32+
}
33+
-- modver.go --
34+
package versions
35+
36+
func At18FromModuleVersion(l []int) {
37+
for i, v := range l {
38+
go func() {
39+
print(i) // want "loop variable i captured by func literal"
40+
print(v) // want "loop variable v captured by func literal"
41+
}()
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Test loopclosure at go version go1.22.
2+
-- go.mod --
3+
module golang.org/fake/versions
4+
5+
go 1.22
6+
-- pre.go --
7+
//go:build go1.19
8+
9+
package versions
10+
11+
func Bad(l []int) {
12+
for i, v := range l {
13+
go func() {
14+
print(i) // want "loop variable i captured by func literal"
15+
print(v) // want "loop variable v captured by func literal"
16+
}()
17+
}
18+
}
19+
-- go22.go --
20+
//go:build go1.22
21+
22+
package versions
23+
24+
func InGo22(l []int) {
25+
for i, v := range l {
26+
go func() {
27+
print(i) // Not reported due to file's GoVersion.
28+
print(v) // Not reported due to file's GoVersion.
29+
}()
30+
}
31+
}
32+
33+
-- modver.go --
34+
package versions
35+
36+
func At22FromTheModuleVersion(l []int) {
37+
for i, v := range l {
38+
go func() {
39+
print(i) // Not reported due to module's GoVersion.
40+
print(v) // Not reported due to module's GoVersion.
41+
}()
42+
}
43+
}

gopls/doc/analyzers.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,12 @@ iteration variable of an enclosing loop, and the loop calls the function
276276
in such a way (e.g. with go or defer) that it may outlive the loop
277277
iteration and possibly observe the wrong value of the variable.
278278

279+
Note: An iteration variable can only outlive a loop iteration in Go versions <=1.21.
280+
In Go 1.22 and later, the loop variable lifetimes changed to create a new
281+
iteration variable per loop iteration. (See go.dev/issue/60078.)
282+
279283
In this example, all the deferred functions run after the loop has
280-
completed, so all observe the final value of v.
284+
completed, so all observe the final value of v [<go1.22].
281285

282286
for _, v := range list {
283287
defer func() {
@@ -294,7 +298,10 @@ One fix is to create a new variable for each iteration of the loop:
294298
}()
295299
}
296300

297-
The next example uses a go statement and has a similar problem.
301+
After Go version 1.22, the previous two for loops are equivalent
302+
and both are correct.
303+
304+
The next example uses a go statement and has a similar problem [<go1.22].
298305
In addition, it has a data race because the loop updates v
299306
concurrent with the goroutines accessing it.
300307

@@ -318,7 +325,7 @@ A hard-to-spot variant of this form is common in parallel tests:
318325
}
319326

320327
The t.Parallel() call causes the rest of the function to execute
321-
concurrent with the loop.
328+
concurrent with the loop [<go1.22].
322329

323330
The analyzer reports references only in the last statement,
324331
as it is not deep enough to understand the effects of subsequent

gopls/internal/lsp/source/api_json.go

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)