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