Skip to content

Commit 4317959

Browse files
committed
go/analysis/passes/waitgroup: report WaitGroup.Add in goroutine
This CL defines a new analyzer, "waitgroup", that reports a common mistake with sync.WaitGroup: calling wg.Add(1) inside the new goroutine, instead of before starting it. This is a port of Dominik Honnef's SA2000 algorithm, which uses tree-based pattern matching, to elementary go/{ast,types} + inspector operations. Fixes golang/go#18022 Updates golang/go#63796 Change-Id: I9d6d3b602ce963912422ee0459bb1f9522fc51f9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/632915 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 72fdfa6 commit 4317959

File tree

6 files changed

+185
-5
lines changed

6 files changed

+185
-5
lines changed

go/analysis/passes/waitgroup/doc.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 waitgroup defines an Analyzer that detects simple misuses
6+
// of sync.WaitGroup.
7+
//
8+
// # Analyzer waitgroup
9+
//
10+
// waitgroup: check for misuses of sync.WaitGroup
11+
//
12+
// This analyzer detects mistaken calls to the (*sync.WaitGroup).Add
13+
// method from inside a new goroutine, causing Add to race with Wait:
14+
//
15+
// // WRONG
16+
// var wg sync.WaitGroup
17+
// go func() {
18+
// wg.Add(1) // "WaitGroup.Add called from inside new goroutine"
19+
// defer wg.Done()
20+
// ...
21+
// }()
22+
// wg.Wait() // (may return prematurely before new goroutine starts)
23+
//
24+
// The correct code calls Add before starting the goroutine:
25+
//
26+
// // RIGHT
27+
// var wg sync.WaitGroup
28+
// wg.Add(1)
29+
// go func() {
30+
// defer wg.Done()
31+
// ...
32+
// }()
33+
// wg.Wait()
34+
package waitgroup

go/analysis/passes/waitgroup/main.go

Lines changed: 16 additions & 0 deletions
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 waitgroup command applies the golang.org/x/tools/go/analysis/passes/waitgroup
8+
// analysis to the specified packages of Go source code.
9+
package main
10+
11+
import (
12+
"golang.org/x/tools/go/analysis/passes/waitgroup"
13+
"golang.org/x/tools/go/analysis/singlechecker"
14+
)
15+
16+
func main() { singlechecker.Main(waitgroup.Analyzer) }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package a
2+
3+
import "sync"
4+
5+
func f() {
6+
var wg sync.WaitGroup
7+
wg.Add(1) // ok
8+
go func() {
9+
wg.Add(1) // want "WaitGroup.Add called from inside new goroutine"
10+
// ...
11+
wg.Add(1) // ok
12+
}()
13+
wg.Add(1) // ok
14+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2023 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 waitgroup defines an Analyzer that detects simple misuses
6+
// of sync.WaitGroup.
7+
package waitgroup
8+
9+
import (
10+
_ "embed"
11+
"go/ast"
12+
"go/types"
13+
"reflect"
14+
15+
"golang.org/x/tools/go/analysis"
16+
"golang.org/x/tools/go/analysis/passes/inspect"
17+
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
18+
"golang.org/x/tools/go/ast/inspector"
19+
"golang.org/x/tools/go/types/typeutil"
20+
"golang.org/x/tools/internal/typesinternal"
21+
)
22+
23+
//go:embed doc.go
24+
var doc string
25+
26+
var Analyzer = &analysis.Analyzer{
27+
Name: "waitgroup",
28+
Doc: analysisutil.MustExtractDoc(doc, "waitgroup"),
29+
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/waitgroup",
30+
Requires: []*analysis.Analyzer{inspect.Analyzer},
31+
Run: run,
32+
}
33+
34+
func run(pass *analysis.Pass) (any, error) {
35+
if !analysisutil.Imports(pass.Pkg, "sync") {
36+
return nil, nil // doesn't directly import sync
37+
}
38+
39+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
40+
nodeFilter := []ast.Node{
41+
(*ast.CallExpr)(nil),
42+
}
43+
44+
inspect.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
45+
if push {
46+
call := n.(*ast.CallExpr)
47+
if fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func); ok &&
48+
isMethodNamed(fn, "sync", "WaitGroup", "Add") &&
49+
hasSuffix(stack, wantSuffix) &&
50+
backindex(stack, 1) == backindex(stack, 2).(*ast.BlockStmt).List[0] { // ExprStmt must be Block's first stmt
51+
52+
pass.Reportf(call.Lparen, "WaitGroup.Add called from inside new goroutine")
53+
}
54+
}
55+
return true
56+
})
57+
58+
return nil, nil
59+
}
60+
61+
// go func() {
62+
// wg.Add(1)
63+
// ...
64+
// }()
65+
var wantSuffix = []ast.Node{
66+
(*ast.GoStmt)(nil),
67+
(*ast.CallExpr)(nil),
68+
(*ast.FuncLit)(nil),
69+
(*ast.BlockStmt)(nil),
70+
(*ast.ExprStmt)(nil),
71+
(*ast.CallExpr)(nil),
72+
}
73+
74+
// hasSuffix reports whether stack has the matching suffix,
75+
// considering only node types.
76+
func hasSuffix(stack, suffix []ast.Node) bool {
77+
// TODO(adonovan): the inspector could implement this for us.
78+
if len(stack) < len(suffix) {
79+
return false
80+
}
81+
for i := range len(suffix) {
82+
if reflect.TypeOf(backindex(stack, i)) != reflect.TypeOf(backindex(suffix, i)) {
83+
return false
84+
}
85+
}
86+
return true
87+
}
88+
89+
// isMethodNamed reports whether f is a method with the specified
90+
// package, receiver type, and method names.
91+
func isMethodNamed(fn *types.Func, pkg, recv, name string) bool {
92+
if fn.Pkg() != nil && fn.Pkg().Path() == pkg && fn.Name() == name {
93+
if r := fn.Type().(*types.Signature).Recv(); r != nil {
94+
if _, gotRecv := typesinternal.ReceiverNamed(r); gotRecv != nil {
95+
return gotRecv.Obj().Name() == recv
96+
}
97+
}
98+
}
99+
return false
100+
}
101+
102+
// backindex is like [slices.Index] but from the back of the slice.
103+
func backindex[T any](slice []T, i int) T {
104+
return slice[len(slice)-1-i]
105+
}
Lines changed: 16 additions & 0 deletions
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+
package waitgroup_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/go/analysis/passes/waitgroup"
12+
)
13+
14+
func Test(t *testing.T) {
15+
analysistest.Run(t, analysistest.TestData(), waitgroup.Analyzer, "a")
16+
}

gopls/internal/util/typesutil/typesutil.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,3 @@ func EnclosingSignature(path []ast.Node, info *types.Info) *types.Signature {
257257
}
258258
return nil
259259
}
260-
261-
func is[T any](x any) bool {
262-
_, ok := x.(T)
263-
return ok
264-
}

0 commit comments

Comments
 (0)