1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
|
// Copyright 2020 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 testinggoroutine
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
"golang.org/x/tools/go/ast/inspector"
)
const Doc = `report calls to (*testing.T).Fatal from goroutines started by a test.
Functions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and
Skip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.
This checker detects calls to these functions that occur within a goroutine
started by the test. For example:
func TestFoo(t *testing.T) {
go func() {
t.Fatal("oops") // error: (*T).Fatal called from non-test goroutine
}()
}
`
var Analyzer = &analysis.Analyzer{
Name: "testinggoroutine",
Doc: Doc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
var forbidden = map[string]bool{
"FailNow": true,
"Fatal": true,
"Fatalf": true,
"Skip": true,
"Skipf": true,
"SkipNow": true,
}
func run(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
if !analysisutil.Imports(pass.Pkg, "testing") {
return nil, nil
}
// Filter out anything that isn't a function declaration.
onlyFuncs := []ast.Node{
(*ast.FuncDecl)(nil),
}
inspect.Nodes(onlyFuncs, func(node ast.Node, push bool) bool {
fnDecl, ok := node.(*ast.FuncDecl)
if !ok {
return false
}
if !hasBenchmarkOrTestParams(fnDecl) {
return false
}
// Now traverse the benchmark/test's body and check that none of the
// forbidden methods are invoked in the goroutines within the body.
ast.Inspect(fnDecl, func(n ast.Node) bool {
goStmt, ok := n.(*ast.GoStmt)
if !ok {
return true
}
checkGoStmt(pass, goStmt)
// No need to further traverse the GoStmt since right
// above we manually traversed it in the ast.Inspect(goStmt, ...)
return false
})
return false
})
return nil, nil
}
func hasBenchmarkOrTestParams(fnDecl *ast.FuncDecl) bool {
// Check that the function's arguments include "*testing.T" or "*testing.B".
params := fnDecl.Type.Params.List
for _, param := range params {
if _, ok := typeIsTestingDotTOrB(param.Type); ok {
return true
}
}
return false
}
func typeIsTestingDotTOrB(expr ast.Expr) (string, bool) {
starExpr, ok := expr.(*ast.StarExpr)
if !ok {
return "", false
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr)
if !ok {
return "", false
}
varPkg := selExpr.X.(*ast.Ident)
if varPkg.Name != "testing" {
return "", false
}
varTypeName := selExpr.Sel.Name
ok = varTypeName == "B" || varTypeName == "T"
return varTypeName, ok
}
// checkGoStmt traverses the goroutine and checks for the
// use of the forbidden *testing.(B, T) methods.
func checkGoStmt(pass *analysis.Pass, goStmt *ast.GoStmt) {
// Otherwise examine the goroutine to check for the forbidden methods.
ast.Inspect(goStmt, func(n ast.Node) bool {
selExpr, ok := n.(*ast.SelectorExpr)
if !ok {
return true
}
_, bad := forbidden[selExpr.Sel.Name]
if !bad {
return true
}
// Now filter out false positives by the import-path/type.
ident, ok := selExpr.X.(*ast.Ident)
if !ok {
return true
}
if ident.Obj == nil || ident.Obj.Decl == nil {
return true
}
field, ok := ident.Obj.Decl.(*ast.Field)
if !ok {
return true
}
if typeName, ok := typeIsTestingDotTOrB(field.Type); ok {
pass.ReportRangef(selExpr, "call to (*%s).%s from a non-test goroutine", typeName, selExpr.Sel)
}
return true
})
}
|