Marvin Preuss
d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
417 lines
9.7 KiB
Go
417 lines
9.7 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"go/types"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gostaticanalysis/analysisutil"
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/inspect"
|
|
"golang.org/x/tools/go/ast/inspector"
|
|
)
|
|
|
|
const (
|
|
doc = "thelper detects tests helpers which is not start with t.Helper() method."
|
|
checksDoc = `coma separated list of enabled checks
|
|
|
|
Available checks
|
|
|
|
` + checkTBegin + ` - check t.Helper() begins helper function
|
|
` + checkTFirst + ` - check *testing.T is first param of helper function
|
|
` + checkTName + ` - check *testing.T param has t name
|
|
|
|
Also available similar checks for benchmark and TB helpers: ` +
|
|
checkBBegin + `, ` + checkBFirst + `, ` + checkBName + `,` +
|
|
checkTBBegin + `, ` + checkTBFirst + `, ` + checkTBName + `
|
|
|
|
`
|
|
)
|
|
|
|
type enabledChecksValue map[string]struct{}
|
|
|
|
func (m enabledChecksValue) Enabled(c string) bool {
|
|
_, ok := m[c]
|
|
return ok
|
|
}
|
|
|
|
func (m enabledChecksValue) String() string {
|
|
ss := make([]string, 0, len(m))
|
|
for s := range m {
|
|
ss = append(ss, s)
|
|
}
|
|
sort.Strings(ss)
|
|
return strings.Join(ss, ",")
|
|
}
|
|
|
|
func (m enabledChecksValue) Set(s string) error {
|
|
ss := strings.FieldsFunc(s, func(c rune) bool { return c == ',' })
|
|
if len(ss) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for k := range m {
|
|
delete(m, k)
|
|
}
|
|
for _, v := range ss {
|
|
switch v {
|
|
case checkTBegin, checkTFirst, checkTName,
|
|
checkBBegin, checkBFirst, checkBName,
|
|
checkTBBegin, checkTBFirst, checkTBName:
|
|
m[v] = struct{}{}
|
|
default:
|
|
return fmt.Errorf("unknown check name %q (see help for full list)", v)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
checkTBegin = "t_begin"
|
|
checkTFirst = "t_first"
|
|
checkTName = "t_name"
|
|
checkBBegin = "b_begin"
|
|
checkBFirst = "b_first"
|
|
checkBName = "b_name"
|
|
checkTBBegin = "tb_begin"
|
|
checkTBFirst = "tb_first"
|
|
checkTBName = "tb_name"
|
|
)
|
|
|
|
type thelper struct {
|
|
enabledChecks enabledChecksValue
|
|
}
|
|
|
|
// NewAnalyzer return a new thelper analyzer.
|
|
// thelper analyzes Go test codes how they use t.Helper() method.
|
|
func NewAnalyzer() *analysis.Analyzer {
|
|
thelper := thelper{}
|
|
thelper.enabledChecks = enabledChecksValue{
|
|
checkTBegin: struct{}{},
|
|
checkTFirst: struct{}{},
|
|
checkTName: struct{}{},
|
|
checkBBegin: struct{}{},
|
|
checkBFirst: struct{}{},
|
|
checkBName: struct{}{},
|
|
checkTBBegin: struct{}{},
|
|
checkTBFirst: struct{}{},
|
|
checkTBName: struct{}{},
|
|
}
|
|
|
|
a := &analysis.Analyzer{
|
|
Name: "thelper",
|
|
Doc: doc,
|
|
Run: thelper.run,
|
|
Requires: []*analysis.Analyzer{
|
|
inspect.Analyzer,
|
|
},
|
|
}
|
|
|
|
a.Flags.Init("thelper", flag.ExitOnError)
|
|
a.Flags.Var(&thelper.enabledChecks, "checks", checksDoc)
|
|
|
|
return a
|
|
}
|
|
|
|
func (t thelper) run(pass *analysis.Pass) (interface{}, error) {
|
|
tCheckOpts, bCheckOpts, tbCheckOpts, ok := t.buildCheckFuncOpts(pass)
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
var reports reports
|
|
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
|
|
nodeFilter := []ast.Node{
|
|
(*ast.FuncDecl)(nil),
|
|
(*ast.FuncLit)(nil),
|
|
(*ast.CallExpr)(nil),
|
|
}
|
|
inspect.Preorder(nodeFilter, func(node ast.Node) {
|
|
var fd funcDecl
|
|
switch n := node.(type) {
|
|
case *ast.FuncLit:
|
|
fd.Pos = n.Pos()
|
|
fd.Type = n.Type
|
|
fd.Body = n.Body
|
|
fd.Name = ast.NewIdent("")
|
|
case *ast.FuncDecl:
|
|
fd.Pos = n.Name.NamePos
|
|
fd.Type = n.Type
|
|
fd.Body = n.Body
|
|
fd.Name = n.Name
|
|
case *ast.CallExpr:
|
|
tbRunSubtestExpr := extractSubtestExp(pass, n, tCheckOpts.tbRun)
|
|
if tbRunSubtestExpr == nil {
|
|
tbRunSubtestExpr = extractSubtestExp(pass, n, bCheckOpts.tbRun)
|
|
}
|
|
|
|
if tbRunSubtestExpr != nil {
|
|
reports.Filter(funcDefPosition(pass, tbRunSubtestExpr))
|
|
} else {
|
|
reports.NoFilter(funcDefPosition(pass, n.Fun))
|
|
}
|
|
return
|
|
default:
|
|
return
|
|
}
|
|
|
|
checkFunc(pass, &reports, fd, tCheckOpts)
|
|
checkFunc(pass, &reports, fd, bCheckOpts)
|
|
checkFunc(pass, &reports, fd, tbCheckOpts)
|
|
})
|
|
|
|
reports.Flush(pass)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
type checkFuncOpts struct {
|
|
skipPrefix string
|
|
varName string
|
|
tbHelper types.Object
|
|
tbRun types.Object
|
|
tbType types.Type
|
|
ctxType types.Type
|
|
checkBegin bool
|
|
checkFirst bool
|
|
checkName bool
|
|
}
|
|
|
|
func (t thelper) buildCheckFuncOpts(pass *analysis.Pass) (checkFuncOpts, checkFuncOpts, checkFuncOpts, bool) {
|
|
var ctxType types.Type
|
|
ctxObj := analysisutil.ObjectOf(pass, "context", "Context")
|
|
if ctxObj != nil {
|
|
ctxType = ctxObj.Type()
|
|
}
|
|
|
|
tCheckOpts, ok := t.buildTestCheckFuncOpts(pass, ctxType)
|
|
if !ok {
|
|
return checkFuncOpts{}, checkFuncOpts{}, checkFuncOpts{}, false
|
|
}
|
|
|
|
bCheckOpts, ok := t.buildBenchmarkCheckFuncOpts(pass, ctxType)
|
|
if !ok {
|
|
return checkFuncOpts{}, checkFuncOpts{}, checkFuncOpts{}, false
|
|
}
|
|
|
|
tbCheckOpts, ok := t.buildTBCheckFuncOpts(pass, ctxType)
|
|
if !ok {
|
|
return checkFuncOpts{}, checkFuncOpts{}, checkFuncOpts{}, false
|
|
}
|
|
|
|
return tCheckOpts, bCheckOpts, tbCheckOpts, true
|
|
}
|
|
|
|
func (t thelper) buildTestCheckFuncOpts(pass *analysis.Pass, ctxType types.Type) (checkFuncOpts, bool) {
|
|
tObj := analysisutil.ObjectOf(pass, "testing", "T")
|
|
if tObj == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
tHelper, _, _ := types.LookupFieldOrMethod(tObj.Type(), true, tObj.Pkg(), "Helper")
|
|
if tHelper == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
tRun, _, _ := types.LookupFieldOrMethod(tObj.Type(), true, tObj.Pkg(), "Run")
|
|
if tRun == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
return checkFuncOpts{
|
|
skipPrefix: "Test",
|
|
varName: "t",
|
|
tbHelper: tHelper,
|
|
tbRun: tRun,
|
|
tbType: types.NewPointer(tObj.Type()),
|
|
ctxType: ctxType,
|
|
checkBegin: t.enabledChecks.Enabled(checkTBegin),
|
|
checkFirst: t.enabledChecks.Enabled(checkTFirst),
|
|
checkName: t.enabledChecks.Enabled(checkTName),
|
|
}, true
|
|
}
|
|
|
|
func (t thelper) buildBenchmarkCheckFuncOpts(pass *analysis.Pass, ctxType types.Type) (checkFuncOpts, bool) {
|
|
bObj := analysisutil.ObjectOf(pass, "testing", "B")
|
|
if bObj == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
bHelper, _, _ := types.LookupFieldOrMethod(bObj.Type(), true, bObj.Pkg(), "Helper")
|
|
if bHelper == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
bRun, _, _ := types.LookupFieldOrMethod(bObj.Type(), true, bObj.Pkg(), "Run")
|
|
if bRun == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
return checkFuncOpts{
|
|
skipPrefix: "Benchmark",
|
|
varName: "b",
|
|
tbHelper: bHelper,
|
|
tbRun: bRun,
|
|
tbType: types.NewPointer(bObj.Type()),
|
|
ctxType: ctxType,
|
|
checkBegin: t.enabledChecks.Enabled(checkBBegin),
|
|
checkFirst: t.enabledChecks.Enabled(checkBFirst),
|
|
checkName: t.enabledChecks.Enabled(checkBName),
|
|
}, true
|
|
}
|
|
|
|
func (t thelper) buildTBCheckFuncOpts(pass *analysis.Pass, ctxType types.Type) (checkFuncOpts, bool) {
|
|
tbObj := analysisutil.ObjectOf(pass, "testing", "TB")
|
|
if tbObj == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
tbHelper, _, _ := types.LookupFieldOrMethod(tbObj.Type(), true, tbObj.Pkg(), "Helper")
|
|
if tbHelper == nil {
|
|
return checkFuncOpts{}, false
|
|
}
|
|
|
|
return checkFuncOpts{
|
|
skipPrefix: "",
|
|
varName: "tb",
|
|
tbHelper: tbHelper,
|
|
tbType: tbObj.Type(),
|
|
ctxType: ctxType,
|
|
checkBegin: t.enabledChecks.Enabled(checkTBBegin),
|
|
checkFirst: t.enabledChecks.Enabled(checkTBFirst),
|
|
checkName: t.enabledChecks.Enabled(checkTBName),
|
|
}, true
|
|
}
|
|
|
|
type funcDecl struct {
|
|
Pos token.Pos
|
|
Name *ast.Ident
|
|
Type *ast.FuncType
|
|
Body *ast.BlockStmt
|
|
}
|
|
|
|
func checkFunc(pass *analysis.Pass, reports *reports, funcDecl funcDecl, opts checkFuncOpts) {
|
|
if opts.skipPrefix != "" && strings.HasPrefix(funcDecl.Name.Name, opts.skipPrefix) {
|
|
return
|
|
}
|
|
|
|
p, pos, ok := searchFuncParam(pass, funcDecl, opts.tbType)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if opts.checkFirst {
|
|
if pos != 0 {
|
|
checkFirstPassed := false
|
|
if pos == 1 && opts.ctxType != nil {
|
|
_, pos, ok := searchFuncParam(pass, funcDecl, opts.ctxType)
|
|
checkFirstPassed = ok && (pos == 0)
|
|
}
|
|
|
|
if !checkFirstPassed {
|
|
reports.Reportf(funcDecl.Pos, "parameter %s should be the first or after context.Context", opts.tbType)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(p.Names) > 0 && p.Names[0].Name != "_" {
|
|
if opts.checkName {
|
|
if p.Names[0].Name != opts.varName {
|
|
reports.Reportf(funcDecl.Pos, "parameter %s should have name %s", opts.tbType, opts.varName)
|
|
}
|
|
}
|
|
|
|
if opts.checkBegin {
|
|
if len(funcDecl.Body.List) == 0 || !isTHelperCall(pass, funcDecl.Body.List[0], opts.tbHelper) {
|
|
reports.Reportf(funcDecl.Pos, "test helper function should start from %s.Helper()", opts.varName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func searchFuncParam(pass *analysis.Pass, f funcDecl, p types.Type) (*ast.Field, int, bool) {
|
|
for i, f := range f.Type.Params.List {
|
|
typeInfo, ok := pass.TypesInfo.Types[f.Type]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if types.Identical(typeInfo.Type, p) {
|
|
return f, i, true
|
|
}
|
|
}
|
|
return nil, 0, false
|
|
}
|
|
|
|
func isTHelperCall(pass *analysis.Pass, s ast.Stmt, tHelper types.Object) bool {
|
|
exprStmt, ok := s.(*ast.ExprStmt)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
callExpr, ok := exprStmt.X.(*ast.CallExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return isSelectorCall(pass, selExpr, tHelper)
|
|
}
|
|
|
|
func extractSubtestExp(pass *analysis.Pass, e *ast.CallExpr, tbRun types.Object) ast.Expr {
|
|
selExpr, ok := e.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if !isSelectorCall(pass, selExpr, tbRun) {
|
|
return nil
|
|
}
|
|
|
|
if len(e.Args) != 2 {
|
|
return nil
|
|
}
|
|
|
|
return e.Args[1]
|
|
}
|
|
|
|
func funcDefPosition(pass *analysis.Pass, e ast.Expr) token.Pos {
|
|
anonFunLit, ok := e.(*ast.FuncLit)
|
|
if ok {
|
|
return anonFunLit.Pos()
|
|
}
|
|
|
|
funIdent, ok := e.(*ast.Ident)
|
|
if !ok {
|
|
selExpr, ok := e.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return token.NoPos
|
|
}
|
|
funIdent = selExpr.Sel
|
|
}
|
|
|
|
funDef, ok := pass.TypesInfo.Uses[funIdent]
|
|
if !ok {
|
|
return token.NoPos
|
|
}
|
|
|
|
return funDef.Pos()
|
|
}
|
|
|
|
func isSelectorCall(pass *analysis.Pass, selExpr *ast.SelectorExpr, callObj types.Object) bool {
|
|
sel, ok := pass.TypesInfo.Selections[selExpr]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return sel.Obj() == callObj
|
|
}
|