| // 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 main |
| |
| import ( |
| "go/ast" |
| "go/types" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| ) |
| |
| func init() { |
| register("tests", |
| "check for common mistaken usages of tests/documentation examples", |
| checkTestFunctions, |
| funcDecl) |
| } |
| |
| 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(name string, scopes []*types.Scope) types.Object { |
| for _, scope := range scopes { |
| if o := scope.Lookup(name); o != nil { |
| return o |
| } |
| } |
| return nil |
| } |
| |
| func extendedScope(f *File) []*types.Scope { |
| scopes := []*types.Scope{f.pkg.typesPkg.Scope()} |
| if f.basePkg != nil { |
| scopes = append(scopes, f.basePkg.typesPkg.Scope()) |
| } else { |
| // If basePkg is not specified (e.g. when checking a single file) try to |
| // find it among imports. |
| pkgName := f.pkg.typesPkg.Name() |
| if strings.HasSuffix(pkgName, "_test") { |
| basePkgName := strings.TrimSuffix(pkgName, "_test") |
| for _, p := range f.pkg.typesPkg.Imports() { |
| if p.Name() == basePkgName { |
| scopes = append(scopes, p.Scope()) |
| break |
| } |
| } |
| } |
| } |
| return scopes |
| } |
| |
| func checkExample(fn *ast.FuncDecl, f *File, report reporter) { |
| fnName := fn.Name.Name |
| if params := fn.Type.Params; len(params.List) != 0 { |
| report("%s should be niladic", fnName) |
| } |
| if results := fn.Type.Results; results != nil && len(results.List) != 0 { |
| report("%s should return nothing", fnName) |
| } |
| |
| if filesRun && !includesNonTest { |
| // The coherence checks between a test and the package it tests |
| // will report false positives if no non-test files have |
| // been provided. |
| return |
| } |
| |
| if fnName == "Example" { |
| // Nothing more to do. |
| return |
| } |
| |
| var ( |
| exName = strings.TrimPrefix(fnName, "Example") |
| elems = strings.SplitN(exName, "_", 3) |
| ident = elems[0] |
| obj = lookup(ident, extendedScope(f)) |
| ) |
| if ident != "" && obj == nil { |
| // Check ExampleFoo and ExampleBadFoo. |
| report("%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) { |
| report("%s has malformed example suffix: %s", fnName, residual) |
| } |
| return |
| } |
| |
| mmbr := elems[1] |
| if !isExampleSuffix(mmbr) { |
| // Check ExampleFoo_Method and ExampleFoo_BadMethod. |
| if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj == nil { |
| report("%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. |
| report("%s has malformed example suffix: %s", fnName, elems[2]) |
| } |
| } |
| |
| func checkTest(fn *ast.FuncDecl, prefix string, report reporter) { |
| // 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):]) { |
| report("%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) |
| } |
| } |
| |
| type reporter func(format string, args ...interface{}) |
| |
| // checkTestFunctions walks Test, Benchmark and Example functions checking |
| // malformed names, wrong signatures and examples documenting nonexistent |
| // identifiers. |
| func checkTestFunctions(f *File, node ast.Node) { |
| if !strings.HasSuffix(f.name, "_test.go") { |
| return |
| } |
| |
| fn, ok := node.(*ast.FuncDecl) |
| if !ok || fn.Recv != nil { |
| // Ignore non-functions or functions with receivers. |
| return |
| } |
| |
| report := func(format string, args ...interface{}) { f.Badf(node.Pos(), format, args...) } |
| |
| switch { |
| case strings.HasPrefix(fn.Name.Name, "Example"): |
| checkExample(fn, f, report) |
| case strings.HasPrefix(fn.Name.Name, "Test"): |
| checkTest(fn, "Test", report) |
| case strings.HasPrefix(fn.Name.Name, "Benchmark"): |
| checkTest(fn, "Benchmark", report) |
| } |
| } |