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