diff options
Diffstat (limited to 'go/analysis/passes/loopclosure/testdata/src')
3 files changed, 338 insertions, 4 deletions
diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 2c8e2e6c4..7a7f05f66 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -6,7 +6,13 @@ package testdata -import "golang.org/x/sync/errgroup" +import ( + "sync" + + "golang.org/x/sync/errgroup" +) + +var A int func _() { var s []int @@ -49,6 +55,19 @@ func _() { println(i, v) }() } + + // iteration variable declared outside the loop + for A = range s { + go func() { + println(A) // want "loop variable A captured by func literal" + }() + } + // iteration variable declared in a different file + for B = range s { + go func() { + println(B) // want "loop variable B captured by func literal" + }() + } // If the key of the range statement is not an identifier // the code should not panic (it used to). var x [2]int @@ -91,9 +110,73 @@ func _() { } } -// Group is used to test that loopclosure does not match on any type named "Group". -// The checker only matches on methods "(*...errgroup.Group).Go". -type Group struct{}; +// Cases that rely on recursively checking for last statements. +func _() { + + for i := range "outer" { + for j := range "inner" { + if j < 1 { + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + } else if j < 2 { + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } else { + go func() { + print(i) + }() + println("we don't catch the error above because of this statement") + } + } + } + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if j < 1 { + switch j { + case 0: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } else if j < 2 { + var a interface{} = j + switch a.(type) { + case int: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } else { + ch := make(chan string) + select { + case <-ch: + defer func() { + print(i) // want "loop variable i captured by func literal" + }() + default: + go func() { + print(i) // want "loop variable i captured by func literal" + }() + } + } + } + } +} + +// Group is used to test that loopclosure only matches Group.Go when Group is +// from the golang.org/x/sync/errgroup package. +type Group struct{} func (g *Group) Go(func() error) {} @@ -108,6 +191,21 @@ func _() { return nil }) } + + for i, v := range s { + if i > 0 { + g.Go(func() error { + print(i) // want "loop variable i captured by func literal" + return nil + }) + } else { + g.Go(func() error { + print(v) // want "loop variable v captured by func literal" + return nil + }) + } + } + // Do not match other Group.Go cases g1 := new(Group) for i, v := range s { @@ -118,3 +216,28 @@ func _() { }) } } + +// Real-world example from #16520, slightly simplified +func _() { + var nodes []interface{} + + critical := new(errgroup.Group) + others := sync.WaitGroup{} + + isCritical := func(node interface{}) bool { return false } + run := func(node interface{}) error { return nil } + + for _, node := range nodes { + if isCritical(node) { + critical.Go(func() error { + return run(node) // want "loop variable node captured by func literal" + }) + } else { + others.Add(1) + go func() { + _ = run(node) // want "loop variable node captured by func literal" + others.Done() + }() + } + } +} diff --git a/go/analysis/passes/loopclosure/testdata/src/a/b.go b/go/analysis/passes/loopclosure/testdata/src/a/b.go new file mode 100644 index 000000000..d4e5da418 --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/a/b.go @@ -0,0 +1,9 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package testdata + +// B is declared in a separate file to test that object resolution spans the +// entire package. +var B int diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go new file mode 100644 index 000000000..50283ec61 --- /dev/null +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -0,0 +1,202 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains tests that the loopclosure analyzer detects leaked +// references via parallel subtests. + +package subtests + +import ( + "testing" +) + +// T is used to test that loopclosure only matches T.Run when T is from the +// testing package. +type T struct{} + +// Run should not match testing.T.Run. Note that the second argument is +// intentionally a *testing.T, not a *T, so that we can check both +// testing.T.Parallel inside a T.Run, and a T.Parallel inside a testing.T.Run. +func (t *T) Run(string, func(*testing.T)) { +} + +func (t *T) Parallel() {} + +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + // Check that parallel subtests are identified. + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + + // Check that serial tests are OK. + t.Run("", func(t *testing.T) { + println(i) + println(test) + }) + + // Check that the location of t.Parallel matters. + t.Run("", func(t *testing.T) { + println(i) + println(test) + t.Parallel() + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + + // Check that *testing.T value matters. + t.Run("", func(t *testing.T) { + var x testing.T + x.Parallel() + println(i) + println(test) + }) + + // Check that shadowing the loop variables within the test literal is OK if + // it occurs before t.Parallel(). + t.Run("", func(t *testing.T) { + i := i + test := test + t.Parallel() + println(i) + println(test) + }) + + // Check that shadowing the loop variables within the test literal is Not + // OK if it occurs after t.Parallel(). + t.Run("", func(t *testing.T) { + t.Parallel() + i := i // want "loop variable i captured by func literal" + test := test // want "loop variable test captured by func literal" + println(i) // OK + println(test) // OK + }) + + // Check uses in nested blocks. + t.Run("", func(t *testing.T) { + t.Parallel() + { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + } + }) + + // Check that we catch uses in nested subtests. + t.Run("", func(t *testing.T) { + t.Parallel() + t.Run("", func(t *testing.T) { + println(i) // want "loop variable i captured by func literal" + println(test) // want "loop variable test captured by func literal" + }) + }) + + // Check that there is no diagnostic if t is not a *testing.T. + t.Run("", func(_ *testing.T) { + t := &T{} + t.Parallel() + println(i) + println(test) + }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the call to t.Parallel to have been skipped. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + Test: + println(i) + println(test) + }) + + // Check that there is no diagnostic when a jump to a label may have caused + // the loop variable reference to be skipped, but there is a diagnostic + // when both the call to t.Parallel and the loop variable reference occur + // after the final label in the block. + t.Run("", func(t *testing.T) { + if true { + goto Test + } + t.Parallel() + println(i) // maybe OK + Test: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) + + // Check that multiple labels are handled. + t.Run("", func(t *testing.T) { + if true { + goto Test1 + } else { + goto Test2 + } + Test1: + Test2: + t.Parallel() + println(test) // want "loop variable test captured by func literal" + }) + + // Check that we do not have problems when t.Run has a single argument. + fn := func() (string, func(t *testing.T)) { return "", nil } + t.Run(fn()) + } +} + +// Check that there is no diagnostic when loop variables are shadowed within +// the loop body. +func _(t *testing.T) { + for i, test := range []int{1, 2, 3} { + i := i + test := test + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that t.Run must be *testing.T.Run. +func _(t *T) { + for i, test := range []int{1, 2, 3} { + t.Run("", func(t *testing.T) { + t.Parallel() + println(i) + println(test) + }) + } +} + +// Check that the top-level must be parallel in order to cause a diagnostic. +// +// From https://pkg.go.dev/testing: +// +// "Run does not return until parallel subtests have completed, providing a +// way to clean up after a group of parallel tests" +func _(t *testing.T) { + for _, test := range []int{1, 2, 3} { + // In this subtest, a/b must complete before the synchronous subtest "a" + // completes, so the reference to test does not escape the current loop + // iteration. + t.Run("a", func(s *testing.T) { + s.Run("b", func(u *testing.T) { + u.Parallel() + println(test) + }) + }) + + // In this subtest, c executes concurrently, so the reference to test may + // escape the current loop iteration. + t.Run("c", func(s *testing.T) { + s.Parallel() + s.Run("d", func(u *testing.T) { + println(test) // want "loop variable test captured by func literal" + }) + }) + } +} |