// Copyright 2015 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 tests defines an Analyzer that checks for common mistaken // usages of tests and examples. package tests import ( "go/ast" "go/types" "strings" "unicode" "unicode/utf8" "golang.org/x/tools/go/analysis" ) const Doc = `check for common mistaken usages of tests and examples The tests checker walks Test, Benchmark and Example functions checking malformed names, wrong signatures and examples documenting non-existent identifiers. Please see the documentation for package testing in golang.org/pkg/testing for the conventions that are enforced for Tests, Benchmarks, and Examples.` var Analyzer = &analysis.Analyzer{ Name: "tests", Doc: Doc, Run: run, } func run(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") { continue } for _, decl := range f.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok || fn.Recv != nil { // Ignore non-functions or functions with receivers. continue } switch { case strings.HasPrefix(fn.Name.Name, "Example"): checkExample(pass, fn) case strings.HasPrefix(fn.Name.Name, "Test"): checkTest(pass, fn, "Test") case strings.HasPrefix(fn.Name.Name, "Benchmark"): checkTest(pass, fn, "Benchmark") } } } return nil, nil } func isExampleSuffix(s string) bool { r, size := utf8.DecodeRuneInString(s) return size > 0 && unicode.IsLower(r) } func isTestSuffix(name string) bool { if len(name) == 0 { // "Test" is ok. return true } r, _ := utf8.DecodeRuneInString(name) return !unicode.IsLower(r) } func isTestParam(typ ast.Expr, wantType string) bool { ptr, ok := typ.(*ast.StarExpr) if !ok { // Not a pointer. return false } // No easy way of making sure it's a *testing.T or *testing.B: // ensure the name of the type matches. if name, ok := ptr.X.(*ast.Ident); ok { return name.Name == wantType } if sel, ok := ptr.X.(*ast.SelectorExpr); ok { return sel.Sel.Name == wantType } return false } func lookup(pkg *types.Package, name string) []types.Object { if o := pkg.Scope().Lookup(name); o != nil { return []types.Object{o} } var ret []types.Object // Search through the imports to see if any of them define name. // It's hard to tell in general which package is being tested, so // for the purposes of the analysis, allow the object to appear // in any of the imports. This guarantees there are no false positives // because the example needs to use the object so it must be defined // in the package or one if its imports. On the other hand, false // negatives are possible, but should be rare. for _, imp := range pkg.Imports() { if obj := imp.Scope().Lookup(name); obj != nil { ret = append(ret, obj) } } return ret } func checkExample(pass *analysis.Pass, fn *ast.FuncDecl) { fnName := fn.Name.Name if params := fn.Type.Params; len(params.List) != 0 { pass.Reportf(fn.Pos(), "%s should be niladic", fnName) } if results := fn.Type.Results; results != nil && len(results.List) != 0 { pass.Reportf(fn.Pos(), "%s should return nothing", fnName) } if fnName == "Example" { // Nothing more to do. return } var ( exName = strings.TrimPrefix(fnName, "Example") elems = strings.SplitN(exName, "_", 3) ident = elems[0] objs = lookup(pass.Pkg, ident) ) if ident != "" && len(objs) == 0 { // Check ExampleFoo and ExampleBadFoo. pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) // Abort since obj is absent and no subsequent checks can be performed. return } if len(elems) < 2 { // Nothing more to do. return } if ident == "" { // Check Example_suffix and Example_BadSuffix. if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) } return } mmbr := elems[1] if !isExampleSuffix(mmbr) { // Check ExampleFoo_Method and ExampleFoo_BadMethod. found := false // Check if Foo.Method exists in this package or its imports. for _, obj := range objs { if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil { found = true break } } if !found { pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) } } if len(elems) == 3 && !isExampleSuffix(elems[2]) { // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) } } func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { // Want functions with 0 results and 1 parameter. if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || fn.Type.Params == nil || len(fn.Type.Params.List) != 1 || len(fn.Type.Params.List[0].Names) > 1 { return } // The param must look like a *testing.T or *testing.B. if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { return } if !isTestSuffix(fn.Name.Name[len(prefix):]) { pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) } }