Skip to content

Commit dd30685

Browse files
authored
Lint when t.Cleanup closes over t (microsoft#3237)
1 parent f367afa commit dd30685

7 files changed

Lines changed: 202 additions & 3 deletions

File tree

_tools/customlint/cleanup.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package customlint
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/types"
7+
8+
"golang.org/x/tools/go/analysis"
9+
"golang.org/x/tools/go/analysis/passes/inspect"
10+
"golang.org/x/tools/go/ast/inspector"
11+
)
12+
13+
var cleanupAnalyzer = &analysis.Analyzer{
14+
Name: "cleanup",
15+
Doc: "finds t.Cleanup calls where the closure captures a testing variable from the enclosing function",
16+
Requires: []*analysis.Analyzer{
17+
inspect.Analyzer,
18+
},
19+
Run: func(pass *analysis.Pass) (any, error) {
20+
return (&cleanupPass{pass: pass}).run()
21+
},
22+
}
23+
24+
type cleanupPass struct {
25+
pass *analysis.Pass
26+
}
27+
28+
func (c *cleanupPass) run() (any, error) {
29+
in := c.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
30+
31+
for cursor := range in.Root().Preorder((*ast.CallExpr)(nil)) {
32+
call := cursor.Node().(*ast.CallExpr)
33+
c.checkCall(call)
34+
}
35+
36+
return nil, nil
37+
}
38+
39+
func (c *cleanupPass) checkCall(call *ast.CallExpr) {
40+
sel, ok := call.Fun.(*ast.SelectorExpr)
41+
if !ok || sel.Sel.Name != "Cleanup" {
42+
return
43+
}
44+
45+
recvType := c.pass.TypesInfo.TypeOf(sel.X)
46+
if recvType == nil || !isTestingType(recvType) {
47+
return
48+
}
49+
50+
if len(call.Args) != 1 {
51+
return
52+
}
53+
funcLit, ok := call.Args[0].(*ast.FuncLit)
54+
if !ok {
55+
return
56+
}
57+
58+
ast.Inspect(funcLit.Body, func(n ast.Node) bool {
59+
c.checkCleanupIdent(n, funcLit)
60+
return true
61+
})
62+
}
63+
64+
func (c *cleanupPass) checkCleanupIdent(n ast.Node, funcLit *ast.FuncLit) {
65+
ident, ok := n.(*ast.Ident)
66+
if !ok {
67+
return
68+
}
69+
obj := c.pass.TypesInfo.Uses[ident]
70+
if obj == nil {
71+
return
72+
}
73+
v, ok := obj.(*types.Var)
74+
if !ok {
75+
return
76+
}
77+
if !isTestingType(v.Type()) {
78+
return
79+
}
80+
// Flag if the variable is defined outside the closure (captured).
81+
if v.Pos() < funcLit.Pos() || v.Pos() >= funcLit.End() {
82+
c.pass.Report(analysis.Diagnostic{
83+
Pos: ident.Pos(),
84+
End: ident.End(),
85+
Message: fmt.Sprintf("cleanup closure captures %s; the test will have ended when cleanup runs", ident.Name),
86+
})
87+
}
88+
}
89+
90+
func isTestingType(t types.Type) bool {
91+
if ptr, ok := t.(*types.Pointer); ok {
92+
t = ptr.Elem()
93+
}
94+
named, ok := t.(*types.Named)
95+
if !ok {
96+
return false
97+
}
98+
obj := named.Obj()
99+
if obj.Pkg() == nil || obj.Pkg().Path() != "testing" {
100+
return false
101+
}
102+
switch obj.Name() {
103+
case "T", "B", "F", "TB":
104+
return true
105+
}
106+
return false
107+
}

_tools/customlint/plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type plugin struct{}
1616
func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
1717
return []*analysis.Analyzer{
1818
bitclearAnalyzer,
19+
cleanupAnalyzer,
1920
emptyCaseAnalyzer,
2021
forbidParentAccessAnalyzer,
2122
shadowAnalyzer,

_tools/customlint/plugin_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestPlugin(t *testing.T) {
3333
var plugin plugin
3434

3535
config := &packages.Config{
36-
Mode: packages.LoadSyntax,
36+
Mode: packages.LoadAllSyntax,
3737
Dir: testdataDir,
3838
Env: append(os.Environ(), "GO111MODULE=on", "GOPROXY=off", "GOWORK=off"),
3939
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cleanup
2+
3+
import "testing"
4+
5+
func badT(t *testing.T) {
6+
t.Cleanup(func() {
7+
t.Log("done")
8+
})
9+
}
10+
11+
func badB(b *testing.B) {
12+
b.Cleanup(func() {
13+
b.Log("done")
14+
})
15+
}
16+
17+
func badTB(tb testing.TB) {
18+
tb.Cleanup(func() {
19+
tb.Log("done")
20+
})
21+
}
22+
23+
func badMultipleRefs(t *testing.T) {
24+
t.Cleanup(func() {
25+
t.Log("first")
26+
t.Log("second")
27+
})
28+
}
29+
30+
func good(t *testing.T) {
31+
t.Cleanup(func() {
32+
println("no capture")
33+
})
34+
}
35+
36+
func goodNamedFunc(t *testing.T) {
37+
t.Cleanup(namedFunc)
38+
}
39+
40+
func namedFunc() {}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package cleanup
2+
3+
import "testing"
4+
5+
func badT(t *testing.T) {
6+
t.Cleanup(func() {
7+
t.Log("done")
8+
~
9+
!!! cleanup: cleanup closure captures t; the test will have ended when cleanup runs
10+
})
11+
}
12+
13+
func badB(b *testing.B) {
14+
b.Cleanup(func() {
15+
b.Log("done")
16+
~
17+
!!! cleanup: cleanup closure captures b; the test will have ended when cleanup runs
18+
})
19+
}
20+
21+
func badTB(tb testing.TB) {
22+
tb.Cleanup(func() {
23+
tb.Log("done")
24+
~~
25+
!!! cleanup: cleanup closure captures tb; the test will have ended when cleanup runs
26+
})
27+
}
28+
29+
func badMultipleRefs(t *testing.T) {
30+
t.Cleanup(func() {
31+
t.Log("first")
32+
~
33+
!!! cleanup: cleanup closure captures t; the test will have ended when cleanup runs
34+
t.Log("second")
35+
~
36+
!!! cleanup: cleanup closure captures t; the test will have ended when cleanup runs
37+
})
38+
}
39+
40+
func good(t *testing.T) {
41+
t.Cleanup(func() {
42+
println("no capture")
43+
})
44+
}
45+
46+
func goodNamedFunc(t *testing.T) {
47+
t.Cleanup(namedFunc)
48+
}
49+
50+
func namedFunc() {}
51+

internal/lsp/server_completion_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func initCompletionClient(t *testing.T, files map[string]string, prefs *lsutil.U
4646
FS: fs,
4747
DefaultLibraryPath: bundled.LibPath(),
4848
}, onServerRequest)
49-
t.Cleanup(func() { assert.NilError(t, closeClient()) })
49+
t.Cleanup(func() { _ = closeClient() })
5050

5151
initMsg, _, ok := lsptestutil.SendRequest(t, client, lsproto.InitializeInfo, &lsproto.InitializeParams{
5252
Capabilities: &lsproto.ClientCapabilities{},

internal/lsp/server_projectinfo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func initProjectInfoClient(t *testing.T, files map[string]string) *lsptestutil.L
3737
FS: fs,
3838
DefaultLibraryPath: bundled.LibPath(),
3939
}, onServerRequest)
40-
t.Cleanup(func() { assert.NilError(t, closeClient()) })
40+
t.Cleanup(func() { _ = closeClient() })
4141

4242
initMsg, _, ok := lsptestutil.SendRequest(t, client, lsproto.InitializeInfo, &lsproto.InitializeParams{
4343
Capabilities: &lsproto.ClientCapabilities{},

0 commit comments

Comments
 (0)