1
2
3
4
5
6
7 package tests
8
9 import (
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
22 const Doc = `check for common mistaken usages of tests and examples
23
24 The tests checker walks Test, Benchmark and Example functions checking
25 malformed names, wrong signatures and examples documenting non-existent
26 identifiers.
27
28 Please see the documentation for package testing in golang.org/pkg/testing
29 for the conventions that are enforced for Tests, Benchmarks, and Examples.`
30
31 var Analyzer = &analysis.Analyzer{
32 Name: "tests",
33 Doc: Doc,
34 Run: run,
35 }
36
37 func 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
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
62 func isExampleSuffix(s string) bool {
63 r, size := utf8.DecodeRuneInString(s)
64 return size > 0 && unicode.IsLower(r)
65 }
66
67 func isTestSuffix(name string) bool {
68 if len(name) == 0 {
69
70 return true
71 }
72 r, _ := utf8.DecodeRuneInString(name)
73 return !unicode.IsLower(r)
74 }
75
76 func isTestParam(typ ast.Expr, wantType string) bool {
77 ptr, ok := typ.(*ast.StarExpr)
78 if !ok {
79
80 return false
81 }
82
83
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
93 func 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
100
101
102
103
104
105
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
115 var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)
116
117 type commentMetadata struct {
118 isOutput bool
119 pos token.Pos
120 }
121
122 func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) {
123 commentsInExample := []commentMetadata{}
124 numOutputs := 0
125
126
127
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
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
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
166 func 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
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
191 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident)
192
193 return
194 }
195 if len(elems) < 2 {
196
197 return
198 }
199
200 if ident == "" {
201
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
211 found := false
212
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
225 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2])
226 }
227 }
228
229 func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) {
230
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
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
245
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
View as plain text