1// Copyright 2015 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package tests defines an Analyzer that checks for common mistaken 6// usages of tests and examples. 7package tests 8 9import ( 10 "go/ast" 11 "go/types" 12 "strings" 13 "unicode" 14 "unicode/utf8" 15 16 "golang.org/x/tools/go/analysis" 17) 18 19const Doc = `check for common mistaken usages of tests and examples 20 21The tests checker walks Test, Benchmark and Example functions checking 22malformed names, wrong signatures and examples documenting non-existent 23identifiers.` 24 25var Analyzer = &analysis.Analyzer{ 26 Name: "tests", 27 Doc: Doc, 28 Run: run, 29} 30 31func run(pass *analysis.Pass) (interface{}, error) { 32 for _, f := range pass.Files { 33 if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") { 34 continue 35 } 36 for _, decl := range f.Decls { 37 fn, ok := decl.(*ast.FuncDecl) 38 if !ok || fn.Recv != nil { 39 // Ignore non-functions or functions with receivers. 40 continue 41 } 42 43 switch { 44 case strings.HasPrefix(fn.Name.Name, "Example"): 45 checkExample(pass, fn) 46 case strings.HasPrefix(fn.Name.Name, "Test"): 47 checkTest(pass, fn, "Test") 48 case strings.HasPrefix(fn.Name.Name, "Benchmark"): 49 checkTest(pass, fn, "Benchmark") 50 } 51 } 52 } 53 return nil, nil 54} 55 56func isExampleSuffix(s string) bool { 57 r, size := utf8.DecodeRuneInString(s) 58 return size > 0 && unicode.IsLower(r) 59} 60 61func isTestSuffix(name string) bool { 62 if len(name) == 0 { 63 // "Test" is ok. 64 return true 65 } 66 r, _ := utf8.DecodeRuneInString(name) 67 return !unicode.IsLower(r) 68} 69 70func isTestParam(typ ast.Expr, wantType string) bool { 71 ptr, ok := typ.(*ast.StarExpr) 72 if !ok { 73 // Not a pointer. 74 return false 75 } 76 // No easy way of making sure it's a *testing.T or *testing.B: 77 // ensure the name of the type matches. 78 if name, ok := ptr.X.(*ast.Ident); ok { 79 return name.Name == wantType 80 } 81 if sel, ok := ptr.X.(*ast.SelectorExpr); ok { 82 return sel.Sel.Name == wantType 83 } 84 return false 85} 86 87func lookup(pkg *types.Package, name string) types.Object { 88 if o := pkg.Scope().Lookup(name); o != nil { 89 return o 90 } 91 92 // If this package is ".../foo_test" and it imports a package 93 // ".../foo", try looking in the latter package. 94 // This heuristic should work even on build systems that do not 95 // record any special link between the packages. 96 if basePath := strings.TrimSuffix(pkg.Path(), "_test"); basePath != pkg.Path() { 97 for _, imp := range pkg.Imports() { 98 if imp.Path() == basePath { 99 return imp.Scope().Lookup(name) 100 } 101 } 102 } 103 return nil 104} 105 106func checkExample(pass *analysis.Pass, fn *ast.FuncDecl) { 107 fnName := fn.Name.Name 108 if params := fn.Type.Params; len(params.List) != 0 { 109 pass.Reportf(fn.Pos(), "%s should be niladic", fnName) 110 } 111 if results := fn.Type.Results; results != nil && len(results.List) != 0 { 112 pass.Reportf(fn.Pos(), "%s should return nothing", fnName) 113 } 114 115 if fnName == "Example" { 116 // Nothing more to do. 117 return 118 } 119 120 var ( 121 exName = strings.TrimPrefix(fnName, "Example") 122 elems = strings.SplitN(exName, "_", 3) 123 ident = elems[0] 124 obj = lookup(pass.Pkg, ident) 125 ) 126 if ident != "" && obj == nil { 127 // Check ExampleFoo and ExampleBadFoo. 128 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) 129 // Abort since obj is absent and no subsequent checks can be performed. 130 return 131 } 132 if len(elems) < 2 { 133 // Nothing more to do. 134 return 135 } 136 137 if ident == "" { 138 // Check Example_suffix and Example_BadSuffix. 139 if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { 140 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) 141 } 142 return 143 } 144 145 mmbr := elems[1] 146 if !isExampleSuffix(mmbr) { 147 // Check ExampleFoo_Method and ExampleFoo_BadMethod. 148 if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj == nil { 149 pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) 150 } 151 } 152 if len(elems) == 3 && !isExampleSuffix(elems[2]) { 153 // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. 154 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) 155 } 156} 157 158func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { 159 // Want functions with 0 results and 1 parameter. 160 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 161 fn.Type.Params == nil || 162 len(fn.Type.Params.List) != 1 || 163 len(fn.Type.Params.List[0].Names) > 1 { 164 return 165 } 166 167 // The param must look like a *testing.T or *testing.B. 168 if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { 169 return 170 } 171 172 if !isTestSuffix(fn.Name.Name[len(prefix):]) { 173 pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) 174 } 175} 176