Skip to content

Commit 3a5a051

Browse files
Cuong Manh Lestamblerre
Cuong Manh Le
authored andcommitted
go/analysis/passes: add sigchanyzer Analyzer
This change brings in a static analyzer that was created by Orijtech, Inc. to detect the use of unbuffered channels with signal.Notify, which is a bug that's documented but not enforced. We've found it even in official examples and in the main Go repository as per https://go-review.googlesource.com/c/go/+/274332. We've found that this bug is very common in lots of Go code, hence the reason why we are directly donating it, so that all Go developers running go test, using gopls can use it. Updates golang/go#9399 Change-Id: Ief6d7238dc80bc9fd5f11a585e41387704457276 Reviewed-on: https://go-review.googlesource.com/c/tools/+/274352 gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Rebecca Stambler <[email protected]> Trust: Cuong Manh Le <[email protected]> Trust: Emmanuel Odeke <[email protected]> Run-TryBot: Rebecca Stambler <[email protected]> Reviewed-by: Emmanuel Odeke <[email protected]>
1 parent 123adc8 commit 3a5a051

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2020 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 sigchanyzer defines an Analyzer that detects
6+
// misuse of unbuffered signal as argument to signal.Notify.
7+
package sigchanyzer
8+
9+
import (
10+
"bytes"
11+
"go/ast"
12+
"go/format"
13+
"go/token"
14+
"go/types"
15+
16+
"golang.org/x/tools/go/analysis"
17+
"golang.org/x/tools/go/analysis/passes/inspect"
18+
"golang.org/x/tools/go/ast/inspector"
19+
)
20+
21+
const Doc = `check for unbuffered channel of os.Signal
22+
23+
This checker reports call expression of the form signal.Notify(c <-chan os.Signal, sig ...os.Signal),
24+
where c is an unbuffered channel, which can be at risk of missing the signal.`
25+
26+
// Analyzer describes sigchanyzer analysis function detector.
27+
var Analyzer = &analysis.Analyzer{
28+
Name: "sigchanyzer",
29+
Doc: Doc,
30+
Requires: []*analysis.Analyzer{inspect.Analyzer},
31+
Run: run,
32+
}
33+
34+
func run(pass *analysis.Pass) (interface{}, error) {
35+
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
36+
37+
nodeFilter := []ast.Node{
38+
(*ast.CallExpr)(nil),
39+
}
40+
inspect.Preorder(nodeFilter, func(n ast.Node) {
41+
call := n.(*ast.CallExpr)
42+
if !isSignalNotify(pass.TypesInfo, call) {
43+
return
44+
}
45+
var chanDecl *ast.CallExpr
46+
switch arg := call.Args[0].(type) {
47+
case *ast.Ident:
48+
if decl, ok := findDecl(arg).(*ast.CallExpr); ok {
49+
chanDecl = decl
50+
}
51+
case *ast.CallExpr:
52+
chanDecl = arg
53+
}
54+
if chanDecl == nil || len(chanDecl.Args) != 1 {
55+
return
56+
}
57+
chanDecl.Args = append(chanDecl.Args, &ast.BasicLit{
58+
Kind: token.INT,
59+
Value: "1",
60+
})
61+
var buf bytes.Buffer
62+
if err := format.Node(&buf, token.NewFileSet(), chanDecl); err != nil {
63+
return
64+
}
65+
pass.Report(analysis.Diagnostic{
66+
Pos: call.Pos(),
67+
End: call.End(),
68+
Message: "misuse of unbuffered os.Signal channel as argument to signal.Notify",
69+
SuggestedFixes: []analysis.SuggestedFix{{
70+
Message: "Change to buffer channel",
71+
TextEdits: []analysis.TextEdit{{
72+
Pos: chanDecl.Pos(),
73+
End: chanDecl.End(),
74+
NewText: buf.Bytes(),
75+
}},
76+
}},
77+
})
78+
})
79+
return nil, nil
80+
}
81+
82+
func isSignalNotify(info *types.Info, call *ast.CallExpr) bool {
83+
check := func(id *ast.Ident) bool {
84+
obj := info.ObjectOf(id)
85+
return obj.Name() == "Notify" && obj.Pkg().Path() == "os/signal"
86+
}
87+
switch fun := call.Fun.(type) {
88+
case *ast.SelectorExpr:
89+
return check(fun.Sel)
90+
case *ast.Ident:
91+
if fun, ok := findDecl(fun).(*ast.SelectorExpr); ok {
92+
return check(fun.Sel)
93+
}
94+
return false
95+
default:
96+
return false
97+
}
98+
}
99+
100+
func findDecl(arg *ast.Ident) ast.Node {
101+
if arg.Obj == nil {
102+
return nil
103+
}
104+
switch as := arg.Obj.Decl.(type) {
105+
case *ast.AssignStmt:
106+
if len(as.Lhs) != len(as.Rhs) {
107+
return nil
108+
}
109+
for i, lhs := range as.Lhs {
110+
lid, ok := lhs.(*ast.Ident)
111+
if !ok {
112+
continue
113+
}
114+
if lid.Obj == arg.Obj {
115+
return as.Rhs[i]
116+
}
117+
}
118+
case *ast.ValueSpec:
119+
if len(as.Names) != len(as.Values) {
120+
return nil
121+
}
122+
for i, name := range as.Names {
123+
if name.Obj == arg.Obj {
124+
return as.Values[i]
125+
}
126+
}
127+
}
128+
return nil
129+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2020 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 sigchanyzer_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/go/analysis/passes/sigchanyzer"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testdata := analysistest.TestData()
16+
analysistest.RunWithSuggestedFixes(t, testdata, sigchanyzer.Analyzer, "a")
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package p
2+
3+
import (
4+
"os"
5+
ao "os"
6+
"os/signal"
7+
)
8+
9+
var c = make(chan os.Signal)
10+
var d = make(chan os.Signal)
11+
12+
func f() {
13+
c := make(chan os.Signal, 1)
14+
signal.Notify(c, os.Interrupt) // ok
15+
_ = <-c
16+
}
17+
18+
func g() {
19+
c := make(chan os.Signal)
20+
signal.Notify(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
21+
_ = <-c
22+
}
23+
24+
func h() {
25+
c := make(chan ao.Signal)
26+
signal.Notify(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
27+
_ = <-c
28+
}
29+
30+
func i() {
31+
signal.Notify(d, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
32+
}
33+
34+
func j() {
35+
c := make(chan os.Signal)
36+
f := signal.Notify
37+
f(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package p
2+
3+
import (
4+
"os"
5+
ao "os"
6+
"os/signal"
7+
)
8+
9+
var c = make(chan os.Signal)
10+
var d = make(chan os.Signal, 1)
11+
12+
func f() {
13+
c := make(chan os.Signal, 1)
14+
signal.Notify(c, os.Interrupt) // ok
15+
_ = <-c
16+
}
17+
18+
func g() {
19+
c := make(chan os.Signal, 1)
20+
signal.Notify(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
21+
_ = <-c
22+
}
23+
24+
func h() {
25+
c := make(chan ao.Signal, 1)
26+
signal.Notify(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
27+
_ = <-c
28+
}
29+
30+
func i() {
31+
signal.Notify(d, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
32+
}
33+
34+
func j() {
35+
c := make(chan os.Signal, 1)
36+
f := signal.Notify
37+
f(c, os.Interrupt) // want "misuse of unbuffered os.Signal channel as argument to signal.Notify"
38+
}

0 commit comments

Comments
 (0)