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/token" 12 "go/types" 13 "regexp" 14 "strings" 15 "unicode" 16 "unicode/utf8" 17 18 "golang.org/x/tools/go/analysis" 19 "golang.org/x/tools/internal/typeparams" 20) 21 22const Doc = `check for common mistaken usages of tests and examples 23 24The tests checker walks Test, Benchmark and Example functions checking 25malformed names, wrong signatures and examples documenting non-existent 26identifiers. 27 28Please see the documentation for package testing in golang.org/pkg/testing 29for the conventions that are enforced for Tests, Benchmarks, and Examples.` 30 31var Analyzer = &analysis.Analyzer{ 32 Name: "tests", 33 Doc: Doc, 34 Run: run, 35} 36 37func run(pass *analysis.Pass) (interface{}, error) { 38 for _, f := range pass.Files { 39 if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") { 40 continue 41 } 42 for _, decl := range f.Decls { 43 fn, ok := decl.(*ast.FuncDecl) 44 if !ok || fn.Recv != nil { 45 // Ignore non-functions or functions with receivers. 46 continue 47 } 48 switch { 49 case strings.HasPrefix(fn.Name.Name, "Example"): 50 checkExampleName(pass, fn) 51 checkExampleOutput(pass, fn, f.Comments) 52 case strings.HasPrefix(fn.Name.Name, "Test"): 53 checkTest(pass, fn, "Test") 54 case strings.HasPrefix(fn.Name.Name, "Benchmark"): 55 checkTest(pass, fn, "Benchmark") 56 } 57 } 58 } 59 return nil, nil 60} 61 62func isExampleSuffix(s string) bool { 63 r, size := utf8.DecodeRuneInString(s) 64 return size > 0 && unicode.IsLower(r) 65} 66 67func isTestSuffix(name string) bool { 68 if len(name) == 0 { 69 // "Test" is ok. 70 return true 71 } 72 r, _ := utf8.DecodeRuneInString(name) 73 return !unicode.IsLower(r) 74} 75 76func isTestParam(typ ast.Expr, wantType string) bool { 77 ptr, ok := typ.(*ast.StarExpr) 78 if !ok { 79 // Not a pointer. 80 return false 81 } 82 // No easy way of making sure it's a *testing.T or *testing.B: 83 // ensure the name of the type matches. 84 if name, ok := ptr.X.(*ast.Ident); ok { 85 return name.Name == wantType 86 } 87 if sel, ok := ptr.X.(*ast.SelectorExpr); ok { 88 return sel.Sel.Name == wantType 89 } 90 return false 91} 92 93func lookup(pkg *types.Package, name string) []types.Object { 94 if o := pkg.Scope().Lookup(name); o != nil { 95 return []types.Object{o} 96 } 97 98 var ret []types.Object 99 // Search through the imports to see if any of them define name. 100 // It's hard to tell in general which package is being tested, so 101 // for the purposes of the analysis, allow the object to appear 102 // in any of the imports. This guarantees there are no false positives 103 // because the example needs to use the object so it must be defined 104 // in the package or one if its imports. On the other hand, false 105 // negatives are possible, but should be rare. 106 for _, imp := range pkg.Imports() { 107 if obj := imp.Scope().Lookup(name); obj != nil { 108 ret = append(ret, obj) 109 } 110 } 111 return ret 112} 113 114// This pattern is taken from /go/src/go/doc/example.go 115var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`) 116 117type commentMetadata struct { 118 isOutput bool 119 pos token.Pos 120} 121 122func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) { 123 commentsInExample := []commentMetadata{} 124 numOutputs := 0 125 126 // Find the comment blocks that are in the example. These comments are 127 // guaranteed to be in order of appearance. 128 for _, cg := range fileComments { 129 if cg.Pos() < fn.Pos() { 130 continue 131 } else if cg.End() > fn.End() { 132 break 133 } 134 135 isOutput := outputRe.MatchString(cg.Text()) 136 if isOutput { 137 numOutputs++ 138 } 139 140 commentsInExample = append(commentsInExample, commentMetadata{ 141 isOutput: isOutput, 142 pos: cg.Pos(), 143 }) 144 } 145 146 // Change message based on whether there are multiple output comment blocks. 147 msg := "output comment block must be the last comment block" 148 if numOutputs > 1 { 149 msg = "there can only be one output comment block per example" 150 } 151 152 for i, cg := range commentsInExample { 153 // Check for output comments that are not the last comment in the example. 154 isLast := (i == len(commentsInExample)-1) 155 if cg.isOutput && !isLast { 156 pass.Report( 157 analysis.Diagnostic{ 158 Pos: cg.pos, 159 Message: msg, 160 }, 161 ) 162 } 163 } 164} 165 166func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) { 167 fnName := fn.Name.Name 168 if params := fn.Type.Params; len(params.List) != 0 { 169 pass.Reportf(fn.Pos(), "%s should be niladic", fnName) 170 } 171 if results := fn.Type.Results; results != nil && len(results.List) != 0 { 172 pass.Reportf(fn.Pos(), "%s should return nothing", fnName) 173 } 174 if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { 175 pass.Reportf(fn.Pos(), "%s should not have type params", fnName) 176 } 177 178 if fnName == "Example" { 179 // Nothing more to do. 180 return 181 } 182 183 var ( 184 exName = strings.TrimPrefix(fnName, "Example") 185 elems = strings.SplitN(exName, "_", 3) 186 ident = elems[0] 187 objs = lookup(pass.Pkg, ident) 188 ) 189 if ident != "" && len(objs) == 0 { 190 // Check ExampleFoo and ExampleBadFoo. 191 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) 192 // Abort since obj is absent and no subsequent checks can be performed. 193 return 194 } 195 if len(elems) < 2 { 196 // Nothing more to do. 197 return 198 } 199 200 if ident == "" { 201 // Check Example_suffix and Example_BadSuffix. 202 if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { 203 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) 204 } 205 return 206 } 207 208 mmbr := elems[1] 209 if !isExampleSuffix(mmbr) { 210 // Check ExampleFoo_Method and ExampleFoo_BadMethod. 211 found := false 212 // Check if Foo.Method exists in this package or its imports. 213 for _, obj := range objs { 214 if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil { 215 found = true 216 break 217 } 218 } 219 if !found { 220 pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) 221 } 222 } 223 if len(elems) == 3 && !isExampleSuffix(elems[2]) { 224 // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. 225 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) 226 } 227} 228 229func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { 230 // Want functions with 0 results and 1 parameter. 231 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 232 fn.Type.Params == nil || 233 len(fn.Type.Params.List) != 1 || 234 len(fn.Type.Params.List[0].Names) > 1 { 235 return 236 } 237 238 // The param must look like a *testing.T or *testing.B. 239 if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { 240 return 241 } 242 243 if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { 244 // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. 245 // We have currently decided to also warn before compilation/package loading. This can help users in IDEs. 246 pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix) 247 } 248 249 if !isTestSuffix(fn.Name.Name[len(prefix):]) { 250 pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) 251 } 252} 253