logginghandler/vendor/github.com/kulti/thelper/pkg/analyzer/analyzer.go
Marvin Preuss d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
build: uses go modules for tool handling
2022-01-14 13:51:56 +01:00

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
}