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. |
7 | package tests |
8 | |
9 | import ( |
10 | "fmt" |
11 | "go/ast" |
12 | "go/token" |
13 | "go/types" |
14 | "regexp" |
15 | "strings" |
16 | "unicode" |
17 | "unicode/utf8" |
18 | |
19 | "golang.org/x/tools/go/analysis" |
20 | "golang.org/x/tools/internal/analysisinternal" |
21 | "golang.org/x/tools/internal/typeparams" |
22 | ) |
23 | |
24 | const Doc = `check for common mistaken usages of tests and examples |
25 | |
26 | The tests checker walks Test, Benchmark and Example functions checking |
27 | malformed names, wrong signatures and examples documenting non-existent |
28 | identifiers. |
29 | |
30 | Please see the documentation for package testing in golang.org/pkg/testing |
31 | for the conventions that are enforced for Tests, Benchmarks, and Examples.` |
32 | |
33 | var Analyzer = &analysis.Analyzer{ |
34 | Name: "tests", |
35 | Doc: Doc, |
36 | Run: run, |
37 | } |
38 | |
39 | var acceptedFuzzTypes = []types.Type{ |
40 | types.Typ[types.String], |
41 | types.Typ[types.Bool], |
42 | types.Typ[types.Float32], |
43 | types.Typ[types.Float64], |
44 | types.Typ[types.Int], |
45 | types.Typ[types.Int8], |
46 | types.Typ[types.Int16], |
47 | types.Typ[types.Int32], |
48 | types.Typ[types.Int64], |
49 | types.Typ[types.Uint], |
50 | types.Typ[types.Uint8], |
51 | types.Typ[types.Uint16], |
52 | types.Typ[types.Uint32], |
53 | types.Typ[types.Uint64], |
54 | types.NewSlice(types.Universe.Lookup("byte").Type()), |
55 | } |
56 | |
57 | func run(pass *analysis.Pass) (interface{}, error) { |
58 | for _, f := range pass.Files { |
59 | if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") { |
60 | continue |
61 | } |
62 | for _, decl := range f.Decls { |
63 | fn, ok := decl.(*ast.FuncDecl) |
64 | if !ok || fn.Recv != nil { |
65 | // Ignore non-functions or functions with receivers. |
66 | continue |
67 | } |
68 | switch { |
69 | case strings.HasPrefix(fn.Name.Name, "Example"): |
70 | checkExampleName(pass, fn) |
71 | checkExampleOutput(pass, fn, f.Comments) |
72 | case strings.HasPrefix(fn.Name.Name, "Test"): |
73 | checkTest(pass, fn, "Test") |
74 | case strings.HasPrefix(fn.Name.Name, "Benchmark"): |
75 | checkTest(pass, fn, "Benchmark") |
76 | } |
77 | // run fuzz tests diagnostics only for 1.18 i.e. when analysisinternal.DiagnoseFuzzTests is turned on. |
78 | if strings.HasPrefix(fn.Name.Name, "Fuzz") && analysisinternal.DiagnoseFuzzTests { |
79 | checkTest(pass, fn, "Fuzz") |
80 | checkFuzz(pass, fn) |
81 | } |
82 | } |
83 | } |
84 | return nil, nil |
85 | } |
86 | |
87 | // checkFuzz checks the contents of a fuzz function. |
88 | func checkFuzz(pass *analysis.Pass, fn *ast.FuncDecl) { |
89 | params := checkFuzzCall(pass, fn) |
90 | if params != nil { |
91 | checkAddCalls(pass, fn, params) |
92 | } |
93 | } |
94 | |
95 | // checkFuzzCall checks the arguments of f.Fuzz() calls: |
96 | // |
97 | // 1. f.Fuzz() should call a function and it should be of type (*testing.F).Fuzz(). |
98 | // 2. The called function in f.Fuzz(func(){}) should not return result. |
99 | // 3. First argument of func() should be of type *testing.T |
100 | // 4. Second argument onwards should be of type []byte, string, bool, byte, |
101 | // rune, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, |
102 | // uint32, uint64 |
103 | // 5. func() must not call any *F methods, e.g. (*F).Log, (*F).Error, (*F).Skip |
104 | // The only *F methods that are allowed in the (*F).Fuzz function are (*F).Failed and (*F).Name. |
105 | // |
106 | // Returns the list of parameters to the fuzz function, if they are valid fuzz parameters. |
107 | func checkFuzzCall(pass *analysis.Pass, fn *ast.FuncDecl) (params *types.Tuple) { |
108 | ast.Inspect(fn, func(n ast.Node) bool { |
109 | call, ok := n.(*ast.CallExpr) |
110 | if ok { |
111 | if !isFuzzTargetDotFuzz(pass, call) { |
112 | return true |
113 | } |
114 | |
115 | // Only one argument (func) must be passed to (*testing.F).Fuzz. |
116 | if len(call.Args) != 1 { |
117 | return true |
118 | } |
119 | expr := call.Args[0] |
120 | if pass.TypesInfo.Types[expr].Type == nil { |
121 | return true |
122 | } |
123 | t := pass.TypesInfo.Types[expr].Type.Underlying() |
124 | tSign, argOk := t.(*types.Signature) |
125 | // Argument should be a function |
126 | if !argOk { |
127 | pass.ReportRangef(expr, "argument to Fuzz must be a function") |
128 | return false |
129 | } |
130 | // ff Argument function should not return |
131 | if tSign.Results().Len() != 0 { |
132 | pass.ReportRangef(expr, "fuzz target must not return any value") |
133 | } |
134 | // ff Argument function should have 1 or more argument |
135 | if tSign.Params().Len() == 0 { |
136 | pass.ReportRangef(expr, "fuzz target must have 1 or more argument") |
137 | return false |
138 | } |
139 | ok := validateFuzzArgs(pass, tSign.Params(), expr) |
140 | if ok && params == nil { |
141 | params = tSign.Params() |
142 | } |
143 | // Inspect the function that was passed as an argument to make sure that |
144 | // there are no calls to *F methods, except for Name and Failed. |
145 | ast.Inspect(expr, func(n ast.Node) bool { |
146 | if call, ok := n.(*ast.CallExpr); ok { |
147 | if !isFuzzTargetDot(pass, call, "") { |
148 | return true |
149 | } |
150 | if !isFuzzTargetDot(pass, call, "Name") && !isFuzzTargetDot(pass, call, "Failed") { |
151 | pass.ReportRangef(call, "fuzz target must not call any *F methods") |
152 | } |
153 | } |
154 | return true |
155 | }) |
156 | // We do not need to look at any calls to f.Fuzz inside of a Fuzz call, |
157 | // since they are not allowed. |
158 | return false |
159 | } |
160 | return true |
161 | }) |
162 | return params |
163 | } |
164 | |
165 | // checkAddCalls checks that the arguments of f.Add calls have the same number and type of arguments as |
166 | // the signature of the function passed to (*testing.F).Fuzz |
167 | func checkAddCalls(pass *analysis.Pass, fn *ast.FuncDecl, params *types.Tuple) { |
168 | ast.Inspect(fn, func(n ast.Node) bool { |
169 | call, ok := n.(*ast.CallExpr) |
170 | if ok { |
171 | if !isFuzzTargetDotAdd(pass, call) { |
172 | return true |
173 | } |
174 | |
175 | // The first argument to function passed to (*testing.F).Fuzz is (*testing.T). |
176 | if len(call.Args) != params.Len()-1 { |
177 | pass.ReportRangef(call, "wrong number of values in call to (*testing.F).Add: %d, fuzz target expects %d", len(call.Args), params.Len()-1) |
178 | return true |
179 | } |
180 | var mismatched []int |
181 | for i, expr := range call.Args { |
182 | if pass.TypesInfo.Types[expr].Type == nil { |
183 | return true |
184 | } |
185 | t := pass.TypesInfo.Types[expr].Type |
186 | if !types.Identical(t, params.At(i+1).Type()) { |
187 | mismatched = append(mismatched, i) |
188 | } |
189 | } |
190 | // If just one of the types is mismatched report for that |
191 | // type only. Otherwise report for the whole call to (*testing.F).Add |
192 | if len(mismatched) == 1 { |
193 | i := mismatched[0] |
194 | expr := call.Args[i] |
195 | t := pass.TypesInfo.Types[expr].Type |
196 | pass.ReportRangef(expr, fmt.Sprintf("mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type())) |
197 | } else if len(mismatched) > 1 { |
198 | var gotArgs, wantArgs []types.Type |
199 | for i := 0; i < len(call.Args); i++ { |
200 | gotArgs, wantArgs = append(gotArgs, pass.TypesInfo.Types[call.Args[i]].Type), append(wantArgs, params.At(i+1).Type()) |
201 | } |
202 | pass.ReportRangef(call, fmt.Sprintf("mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs)) |
203 | } |
204 | } |
205 | return true |
206 | }) |
207 | } |
208 | |
209 | // isFuzzTargetDotFuzz reports whether call is (*testing.F).Fuzz(). |
210 | func isFuzzTargetDotFuzz(pass *analysis.Pass, call *ast.CallExpr) bool { |
211 | return isFuzzTargetDot(pass, call, "Fuzz") |
212 | } |
213 | |
214 | // isFuzzTargetDotAdd reports whether call is (*testing.F).Add(). |
215 | func isFuzzTargetDotAdd(pass *analysis.Pass, call *ast.CallExpr) bool { |
216 | return isFuzzTargetDot(pass, call, "Add") |
217 | } |
218 | |
219 | // isFuzzTargetDot reports whether call is (*testing.F).<name>(). |
220 | func isFuzzTargetDot(pass *analysis.Pass, call *ast.CallExpr, name string) bool { |
221 | if selExpr, ok := call.Fun.(*ast.SelectorExpr); ok { |
222 | if !isTestingType(pass.TypesInfo.Types[selExpr.X].Type, "F") { |
223 | return false |
224 | } |
225 | if name == "" || selExpr.Sel.Name == name { |
226 | return true |
227 | } |
228 | } |
229 | return false |
230 | } |
231 | |
232 | // Validate the arguments of fuzz target. |
233 | func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) bool { |
234 | fLit, isFuncLit := expr.(*ast.FuncLit) |
235 | exprRange := expr |
236 | ok := true |
237 | if !isTestingType(params.At(0).Type(), "T") { |
238 | if isFuncLit { |
239 | exprRange = fLit.Type.Params.List[0].Type |
240 | } |
241 | pass.ReportRangef(exprRange, "the first parameter of a fuzz target must be *testing.T") |
242 | ok = false |
243 | } |
244 | for i := 1; i < params.Len(); i++ { |
245 | if !isAcceptedFuzzType(params.At(i).Type()) { |
246 | if isFuncLit { |
247 | curr := 0 |
248 | for _, field := range fLit.Type.Params.List { |
249 | curr += len(field.Names) |
250 | if i < curr { |
251 | exprRange = field.Type |
252 | break |
253 | } |
254 | } |
255 | } |
256 | pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: "+formatAcceptedFuzzType()) |
257 | ok = false |
258 | } |
259 | } |
260 | return ok |
261 | } |
262 | |
263 | func isTestingType(typ types.Type, testingType string) bool { |
264 | ptr, ok := typ.(*types.Pointer) |
265 | if !ok { |
266 | return false |
267 | } |
268 | named, ok := ptr.Elem().(*types.Named) |
269 | if !ok { |
270 | return false |
271 | } |
272 | obj := named.Obj() |
273 | // obj.Pkg is nil for the error type. |
274 | return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == testingType |
275 | } |
276 | |
277 | // Validate that fuzz target function's arguments are of accepted types. |
278 | func isAcceptedFuzzType(paramType types.Type) bool { |
279 | for _, typ := range acceptedFuzzTypes { |
280 | if types.Identical(typ, paramType) { |
281 | return true |
282 | } |
283 | } |
284 | return false |
285 | } |
286 | |
287 | func formatAcceptedFuzzType() string { |
288 | var acceptedFuzzTypesStrings []string |
289 | for _, typ := range acceptedFuzzTypes { |
290 | acceptedFuzzTypesStrings = append(acceptedFuzzTypesStrings, typ.String()) |
291 | } |
292 | acceptedFuzzTypesMsg := strings.Join(acceptedFuzzTypesStrings, ", ") |
293 | return acceptedFuzzTypesMsg |
294 | } |
295 | |
296 | func isExampleSuffix(s string) bool { |
297 | r, size := utf8.DecodeRuneInString(s) |
298 | return size > 0 && unicode.IsLower(r) |
299 | } |
300 | |
301 | func isTestSuffix(name string) bool { |
302 | if len(name) == 0 { |
303 | // "Test" is ok. |
304 | return true |
305 | } |
306 | r, _ := utf8.DecodeRuneInString(name) |
307 | return !unicode.IsLower(r) |
308 | } |
309 | |
310 | func isTestParam(typ ast.Expr, wantType string) bool { |
311 | ptr, ok := typ.(*ast.StarExpr) |
312 | if !ok { |
313 | // Not a pointer. |
314 | return false |
315 | } |
316 | // No easy way of making sure it's a *testing.T or *testing.B: |
317 | // ensure the name of the type matches. |
318 | if name, ok := ptr.X.(*ast.Ident); ok { |
319 | return name.Name == wantType |
320 | } |
321 | if sel, ok := ptr.X.(*ast.SelectorExpr); ok { |
322 | return sel.Sel.Name == wantType |
323 | } |
324 | return false |
325 | } |
326 | |
327 | func lookup(pkg *types.Package, name string) []types.Object { |
328 | if o := pkg.Scope().Lookup(name); o != nil { |
329 | return []types.Object{o} |
330 | } |
331 | |
332 | var ret []types.Object |
333 | // Search through the imports to see if any of them define name. |
334 | // It's hard to tell in general which package is being tested, so |
335 | // for the purposes of the analysis, allow the object to appear |
336 | // in any of the imports. This guarantees there are no false positives |
337 | // because the example needs to use the object so it must be defined |
338 | // in the package or one if its imports. On the other hand, false |
339 | // negatives are possible, but should be rare. |
340 | for _, imp := range pkg.Imports() { |
341 | if obj := imp.Scope().Lookup(name); obj != nil { |
342 | ret = append(ret, obj) |
343 | } |
344 | } |
345 | return ret |
346 | } |
347 | |
348 | // This pattern is taken from /go/src/go/doc/example.go |
349 | var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`) |
350 | |
351 | type commentMetadata struct { |
352 | isOutput bool |
353 | pos token.Pos |
354 | } |
355 | |
356 | func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) { |
357 | commentsInExample := []commentMetadata{} |
358 | numOutputs := 0 |
359 | |
360 | // Find the comment blocks that are in the example. These comments are |
361 | // guaranteed to be in order of appearance. |
362 | for _, cg := range fileComments { |
363 | if cg.Pos() < fn.Pos() { |
364 | continue |
365 | } else if cg.End() > fn.End() { |
366 | break |
367 | } |
368 | |
369 | isOutput := outputRe.MatchString(cg.Text()) |
370 | if isOutput { |
371 | numOutputs++ |
372 | } |
373 | |
374 | commentsInExample = append(commentsInExample, commentMetadata{ |
375 | isOutput: isOutput, |
376 | pos: cg.Pos(), |
377 | }) |
378 | } |
379 | |
380 | // Change message based on whether there are multiple output comment blocks. |
381 | msg := "output comment block must be the last comment block" |
382 | if numOutputs > 1 { |
383 | msg = "there can only be one output comment block per example" |
384 | } |
385 | |
386 | for i, cg := range commentsInExample { |
387 | // Check for output comments that are not the last comment in the example. |
388 | isLast := (i == len(commentsInExample)-1) |
389 | if cg.isOutput && !isLast { |
390 | pass.Report( |
391 | analysis.Diagnostic{ |
392 | Pos: cg.pos, |
393 | Message: msg, |
394 | }, |
395 | ) |
396 | } |
397 | } |
398 | } |
399 | |
400 | func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) { |
401 | fnName := fn.Name.Name |
402 | if params := fn.Type.Params; len(params.List) != 0 { |
403 | pass.Reportf(fn.Pos(), "%s should be niladic", fnName) |
404 | } |
405 | if results := fn.Type.Results; results != nil && len(results.List) != 0 { |
406 | pass.Reportf(fn.Pos(), "%s should return nothing", fnName) |
407 | } |
408 | if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { |
409 | pass.Reportf(fn.Pos(), "%s should not have type params", fnName) |
410 | } |
411 | |
412 | if fnName == "Example" { |
413 | // Nothing more to do. |
414 | return |
415 | } |
416 | |
417 | var ( |
418 | exName = strings.TrimPrefix(fnName, "Example") |
419 | elems = strings.SplitN(exName, "_", 3) |
420 | ident = elems[0] |
421 | objs = lookup(pass.Pkg, ident) |
422 | ) |
423 | if ident != "" && len(objs) == 0 { |
424 | // Check ExampleFoo and ExampleBadFoo. |
425 | pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) |
426 | // Abort since obj is absent and no subsequent checks can be performed. |
427 | return |
428 | } |
429 | if len(elems) < 2 { |
430 | // Nothing more to do. |
431 | return |
432 | } |
433 | |
434 | if ident == "" { |
435 | // Check Example_suffix and Example_BadSuffix. |
436 | if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { |
437 | pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) |
438 | } |
439 | return |
440 | } |
441 | |
442 | mmbr := elems[1] |
443 | if !isExampleSuffix(mmbr) { |
444 | // Check ExampleFoo_Method and ExampleFoo_BadMethod. |
445 | found := false |
446 | // Check if Foo.Method exists in this package or its imports. |
447 | for _, obj := range objs { |
448 | if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil { |
449 | found = true |
450 | break |
451 | } |
452 | } |
453 | if !found { |
454 | pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) |
455 | } |
456 | } |
457 | if len(elems) == 3 && !isExampleSuffix(elems[2]) { |
458 | // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. |
459 | pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) |
460 | } |
461 | } |
462 | |
463 | func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { |
464 | // Want functions with 0 results and 1 parameter. |
465 | if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || |
466 | fn.Type.Params == nil || |
467 | len(fn.Type.Params.List) != 1 || |
468 | len(fn.Type.Params.List[0].Names) > 1 { |
469 | return |
470 | } |
471 | |
472 | // The param must look like a *testing.T or *testing.B. |
473 | if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { |
474 | return |
475 | } |
476 | |
477 | if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 { |
478 | // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. |
479 | // We have currently decided to also warn before compilation/package loading. This can help users in IDEs. |
480 | // TODO(adonovan): use ReportRangef(tparams). |
481 | pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix) |
482 | } |
483 | |
484 | if !isTestSuffix(fn.Name.Name[len(prefix):]) { |
485 | // TODO(adonovan): use ReportRangef(fn.Name). |
486 | pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) |
487 | } |
488 | } |
489 |
Members