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 25Please see the documentation for package testing in golang.org/pkg/testing 26for the conventions that are enforced for Tests, Benchmarks, and Examples.` 27 28var Analyzer = &analysis.Analyzer{ 29 Name: "tests", 30 Doc: Doc, 31 Run: run, 32} 33 34func run(pass *analysis.Pass) (interface{}, error) { 35 for _, f := range pass.Files { 36 if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") { 37 continue 38 } 39 for _, decl := range f.Decls { 40 fn, ok := decl.(*ast.FuncDecl) 41 if !ok || fn.Recv != nil { 42 // Ignore non-functions or functions with receivers. 43 continue 44 } 45 46 switch { 47 case strings.HasPrefix(fn.Name.Name, "Example"): 48 checkExample(pass, fn) 49 case strings.HasPrefix(fn.Name.Name, "Test"): 50 checkTest(pass, fn, "Test") 51 case strings.HasPrefix(fn.Name.Name, "Benchmark"): 52 checkTest(pass, fn, "Benchmark") 53 } 54 } 55 } 56 return nil, nil 57} 58 59func isExampleSuffix(s string) bool { 60 r, size := utf8.DecodeRuneInString(s) 61 return size > 0 && unicode.IsLower(r) 62} 63 64func isTestSuffix(name string) bool { 65 if len(name) == 0 { 66 // "Test" is ok. 67 return true 68 } 69 r, _ := utf8.DecodeRuneInString(name) 70 return !unicode.IsLower(r) 71} 72 73func isTestParam(typ ast.Expr, wantType string) bool { 74 ptr, ok := typ.(*ast.StarExpr) 75 if !ok { 76 // Not a pointer. 77 return false 78 } 79 // No easy way of making sure it's a *testing.T or *testing.B: 80 // ensure the name of the type matches. 81 if name, ok := ptr.X.(*ast.Ident); ok { 82 return name.Name == wantType 83 } 84 if sel, ok := ptr.X.(*ast.SelectorExpr); ok { 85 return sel.Sel.Name == wantType 86 } 87 return false 88} 89 90func lookup(pkg *types.Package, name string) []types.Object { 91 if o := pkg.Scope().Lookup(name); o != nil { 92 return []types.Object{o} 93 } 94 95 var ret []types.Object 96 // Search through the imports to see if any of them define name. 97 // It's hard to tell in general which package is being tested, so 98 // for the purposes of the analysis, allow the object to appear 99 // in any of the imports. This guarantees there are no false positives 100 // because the example needs to use the object so it must be defined 101 // in the package or one if its imports. On the other hand, false 102 // negatives are possible, but should be rare. 103 for _, imp := range pkg.Imports() { 104 if obj := imp.Scope().Lookup(name); obj != nil { 105 ret = append(ret, obj) 106 } 107 } 108 return ret 109} 110 111func checkExample(pass *analysis.Pass, fn *ast.FuncDecl) { 112 fnName := fn.Name.Name 113 if params := fn.Type.Params; len(params.List) != 0 { 114 pass.Reportf(fn.Pos(), "%s should be niladic", fnName) 115 } 116 if results := fn.Type.Results; results != nil && len(results.List) != 0 { 117 pass.Reportf(fn.Pos(), "%s should return nothing", fnName) 118 } 119 120 if fnName == "Example" { 121 // Nothing more to do. 122 return 123 } 124 125 var ( 126 exName = strings.TrimPrefix(fnName, "Example") 127 elems = strings.SplitN(exName, "_", 3) 128 ident = elems[0] 129 objs = lookup(pass.Pkg, ident) 130 ) 131 if ident != "" && len(objs) == 0 { 132 // Check ExampleFoo and ExampleBadFoo. 133 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) 134 // Abort since obj is absent and no subsequent checks can be performed. 135 return 136 } 137 if len(elems) < 2 { 138 // Nothing more to do. 139 return 140 } 141 142 if ident == "" { 143 // Check Example_suffix and Example_BadSuffix. 144 if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { 145 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) 146 } 147 return 148 } 149 150 mmbr := elems[1] 151 if !isExampleSuffix(mmbr) { 152 // Check ExampleFoo_Method and ExampleFoo_BadMethod. 153 found := false 154 // Check if Foo.Method exists in this package or its imports. 155 for _, obj := range objs { 156 if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil { 157 found = true 158 break 159 } 160 } 161 if !found { 162 pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) 163 } 164 } 165 if len(elems) == 3 && !isExampleSuffix(elems[2]) { 166 // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. 167 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) 168 } 169} 170 171func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { 172 // Want functions with 0 results and 1 parameter. 173 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 174 fn.Type.Params == nil || 175 len(fn.Type.Params.List) != 1 || 176 len(fn.Type.Params.List[0].Names) > 1 { 177 return 178 } 179 180 // The param must look like a *testing.T or *testing.B. 181 if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { 182 return 183 } 184 185 if !isTestSuffix(fn.Name.Name[len(prefix):]) { 186 pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) 187 } 188} 189