diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2021-02-22 20:37:25 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2021-02-22 21:02:12 +0100 |
| commit | fcc6d71be2c3ce7d9305c04fc2e87af554571bac (patch) | |
| tree | b01dbb3d1e2988e28ea158d2d543d603ec0b9569 /vendor/github.com/kulti | |
| parent | 8f23c528ad5a943b9ffec5dcaf332fd0f614006e (diff) | |
go.mod: update golangci-lint to v1.37
Diffstat (limited to 'vendor/github.com/kulti')
| -rw-r--r-- | vendor/github.com/kulti/thelper/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/kulti/thelper/pkg/analyzer/analyzer.go | 391 | ||||
| -rw-r--r-- | vendor/github.com/kulti/thelper/pkg/analyzer/report.go | 44 |
3 files changed, 456 insertions, 0 deletions
diff --git a/vendor/github.com/kulti/thelper/LICENSE b/vendor/github.com/kulti/thelper/LICENSE new file mode 100644 index 000000000..e070215fe --- /dev/null +++ b/vendor/github.com/kulti/thelper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Aleksey Bakin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kulti/thelper/pkg/analyzer/analyzer.go b/vendor/github.com/kulti/thelper/pkg/analyzer/analyzer.go new file mode 100644 index 000000000..369d3ad56 --- /dev/null +++ b/vendor/github.com/kulti/thelper/pkg/analyzer/analyzer.go @@ -0,0 +1,391 @@ +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) { + 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 nil, nil + } + + bCheckOpts, ok := t.buildBenchmarkCheckFuncOpts(pass, ctxType) + if !ok { + return nil, nil + } + + tbCheckOpts, ok := t.buildTBCheckFuncOpts(pass, ctxType) + 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: + reports.Filter(subtestPos(pass, n, tCheckOpts.tbRun)) + reports.Filter(subtestPos(pass, n, bCheckOpts.tbRun)) + 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) 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 subtestPos(pass *analysis.Pass, e *ast.CallExpr, tbRun types.Object) token.Pos { + selExpr, ok := e.Fun.(*ast.SelectorExpr) + if !ok { + return token.NoPos + } + + if !isSelectorCall(pass, selExpr, tbRun) { + return token.NoPos + } + + if len(e.Args) != 2 { + return token.NoPos + } + + anonFunLit, ok := e.Args[1].(*ast.FuncLit) + if ok { + return anonFunLit.Pos() + } + + funIdent, ok := e.Args[1].(*ast.Ident) + if !ok { + return token.NoPos + } + + 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 +} diff --git a/vendor/github.com/kulti/thelper/pkg/analyzer/report.go b/vendor/github.com/kulti/thelper/pkg/analyzer/report.go new file mode 100644 index 000000000..1dd3eec5f --- /dev/null +++ b/vendor/github.com/kulti/thelper/pkg/analyzer/report.go @@ -0,0 +1,44 @@ +package analyzer + +import ( + "go/token" + + "golang.org/x/tools/go/analysis" +) + +type reports struct { + reports []report + filter map[token.Pos]struct{} +} + +type report struct { + pos token.Pos + format string + args []interface{} +} + +func (rr *reports) Reportf(pos token.Pos, format string, args ...interface{}) { + rr.reports = append(rr.reports, report{ + pos: pos, + format: format, + args: args, + }) +} + +func (rr *reports) Filter(pos token.Pos) { + if pos.IsValid() { + if rr.filter == nil { + rr.filter = make(map[token.Pos]struct{}) + } + rr.filter[pos] = struct{}{} + } +} + +func (rr reports) Flush(pass *analysis.Pass) { + for _, r := range rr.reports { + if _, ok := rr.filter[r.pos]; ok { + continue + } + pass.Reportf(r.pos, r.format, r.args...) + } +} |
