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