1
2
3
4
5 package tests
6
7 import (
8 _ "embed"
9 "fmt"
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/go/analysis/passes/internal/analysisutil"
20 )
21
22
23 var doc string
24
25 var Analyzer = &analysis.Analyzer{
26 Name: "tests",
27 Doc: analysisutil.MustExtractDoc(doc, "tests"),
28 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/tests",
29 Run: run,
30 }
31
32 var acceptedFuzzTypes = []types.Type{
33 types.Typ[types.String],
34 types.Typ[types.Bool],
35 types.Typ[types.Float32],
36 types.Typ[types.Float64],
37 types.Typ[types.Int],
38 types.Typ[types.Int8],
39 types.Typ[types.Int16],
40 types.Typ[types.Int32],
41 types.Typ[types.Int64],
42 types.Typ[types.Uint],
43 types.Typ[types.Uint8],
44 types.Typ[types.Uint16],
45 types.Typ[types.Uint32],
46 types.Typ[types.Uint64],
47 types.NewSlice(types.Universe.Lookup("byte").Type()),
48 }
49
50 func run(pass *analysis.Pass) (interface{}, error) {
51 for _, f := range pass.Files {
52 if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") {
53 continue
54 }
55 for _, decl := range f.Decls {
56 fn, ok := decl.(*ast.FuncDecl)
57 if !ok || fn.Recv != nil {
58
59 continue
60 }
61 switch {
62 case strings.HasPrefix(fn.Name.Name, "Example"):
63 checkExampleName(pass, fn)
64 checkExampleOutput(pass, fn, f.Comments)
65 case strings.HasPrefix(fn.Name.Name, "Test"):
66 checkTest(pass, fn, "Test")
67 case strings.HasPrefix(fn.Name.Name, "Benchmark"):
68 checkTest(pass, fn, "Benchmark")
69 case strings.HasPrefix(fn.Name.Name, "Fuzz"):
70 checkTest(pass, fn, "Fuzz")
71 checkFuzz(pass, fn)
72 }
73 }
74 }
75 return nil, nil
76 }
77
78
79 func checkFuzz(pass *analysis.Pass, fn *ast.FuncDecl) {
80 params := checkFuzzCall(pass, fn)
81 if params != nil {
82 checkAddCalls(pass, fn, params)
83 }
84 }
85
86
87
88
89
90
91
92
93
94
95
96
97
98 func checkFuzzCall(pass *analysis.Pass, fn *ast.FuncDecl) (params *types.Tuple) {
99 ast.Inspect(fn, func(n ast.Node) bool {
100 call, ok := n.(*ast.CallExpr)
101 if ok {
102 if !isFuzzTargetDotFuzz(pass, call) {
103 return true
104 }
105
106
107 if len(call.Args) != 1 {
108 return true
109 }
110 expr := call.Args[0]
111 if pass.TypesInfo.Types[expr].Type == nil {
112 return true
113 }
114 t := pass.TypesInfo.Types[expr].Type.Underlying()
115 tSign, argOk := t.(*types.Signature)
116
117 if !argOk {
118 pass.ReportRangef(expr, "argument to Fuzz must be a function")
119 return false
120 }
121
122 if tSign.Results().Len() != 0 {
123 pass.ReportRangef(expr, "fuzz target must not return any value")
124 }
125
126 if tSign.Params().Len() == 0 {
127 pass.ReportRangef(expr, "fuzz target must have 1 or more argument")
128 return false
129 }
130 ok := validateFuzzArgs(pass, tSign.Params(), expr)
131 if ok && params == nil {
132 params = tSign.Params()
133 }
134
135
136 ast.Inspect(expr, func(n ast.Node) bool {
137 if call, ok := n.(*ast.CallExpr); ok {
138 if !isFuzzTargetDot(pass, call, "") {
139 return true
140 }
141 if !isFuzzTargetDot(pass, call, "Name") && !isFuzzTargetDot(pass, call, "Failed") {
142 pass.ReportRangef(call, "fuzz target must not call any *F methods")
143 }
144 }
145 return true
146 })
147
148
149 return false
150 }
151 return true
152 })
153 return params
154 }
155
156
157
158 func checkAddCalls(pass *analysis.Pass, fn *ast.FuncDecl, params *types.Tuple) {
159 ast.Inspect(fn, func(n ast.Node) bool {
160 call, ok := n.(*ast.CallExpr)
161 if ok {
162 if !isFuzzTargetDotAdd(pass, call) {
163 return true
164 }
165
166
167 if len(call.Args) != params.Len()-1 {
168 pass.ReportRangef(call, "wrong number of values in call to (*testing.F).Add: %d, fuzz target expects %d", len(call.Args), params.Len()-1)
169 return true
170 }
171 var mismatched []int
172 for i, expr := range call.Args {
173 if pass.TypesInfo.Types[expr].Type == nil {
174 return true
175 }
176 t := pass.TypesInfo.Types[expr].Type
177 if !types.Identical(t, params.At(i+1).Type()) {
178 mismatched = append(mismatched, i)
179 }
180 }
181
182
183 if len(mismatched) == 1 {
184 i := mismatched[0]
185 expr := call.Args[i]
186 t := pass.TypesInfo.Types[expr].Type
187 pass.ReportRangef(expr, fmt.Sprintf("mismatched type in call to (*testing.F).Add: %v, fuzz target expects %v", t, params.At(i+1).Type()))
188 } else if len(mismatched) > 1 {
189 var gotArgs, wantArgs []types.Type
190 for i := 0; i < len(call.Args); i++ {
191 gotArgs, wantArgs = append(gotArgs, pass.TypesInfo.Types[call.Args[i]].Type), append(wantArgs, params.At(i+1).Type())
192 }
193 pass.ReportRangef(call, fmt.Sprintf("mismatched types in call to (*testing.F).Add: %v, fuzz target expects %v", gotArgs, wantArgs))
194 }
195 }
196 return true
197 })
198 }
199
200
201 func isFuzzTargetDotFuzz(pass *analysis.Pass, call *ast.CallExpr) bool {
202 return isFuzzTargetDot(pass, call, "Fuzz")
203 }
204
205
206 func isFuzzTargetDotAdd(pass *analysis.Pass, call *ast.CallExpr) bool {
207 return isFuzzTargetDot(pass, call, "Add")
208 }
209
210
211 func isFuzzTargetDot(pass *analysis.Pass, call *ast.CallExpr, name string) bool {
212 if selExpr, ok := call.Fun.(*ast.SelectorExpr); ok {
213 if !isTestingType(pass.TypesInfo.Types[selExpr.X].Type, "F") {
214 return false
215 }
216 if name == "" || selExpr.Sel.Name == name {
217 return true
218 }
219 }
220 return false
221 }
222
223
224 func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) bool {
225 fLit, isFuncLit := expr.(*ast.FuncLit)
226 exprRange := expr
227 ok := true
228 if !isTestingType(params.At(0).Type(), "T") {
229 if isFuncLit {
230 exprRange = fLit.Type.Params.List[0].Type
231 }
232 pass.ReportRangef(exprRange, "the first parameter of a fuzz target must be *testing.T")
233 ok = false
234 }
235 for i := 1; i < params.Len(); i++ {
236 if !isAcceptedFuzzType(params.At(i).Type()) {
237 if isFuncLit {
238 curr := 0
239 for _, field := range fLit.Type.Params.List {
240 curr += len(field.Names)
241 if i < curr {
242 exprRange = field.Type
243 break
244 }
245 }
246 }
247 pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: "+formatAcceptedFuzzType())
248 ok = false
249 }
250 }
251 return ok
252 }
253
254 func isTestingType(typ types.Type, testingType string) bool {
255 ptr, ok := typ.(*types.Pointer)
256 if !ok {
257 return false
258 }
259 return analysisutil.IsNamedType(ptr.Elem(), "testing", testingType)
260 }
261
262
263 func isAcceptedFuzzType(paramType types.Type) bool {
264 for _, typ := range acceptedFuzzTypes {
265 if types.Identical(typ, paramType) {
266 return true
267 }
268 }
269 return false
270 }
271
272 func formatAcceptedFuzzType() string {
273 var acceptedFuzzTypesStrings []string
274 for _, typ := range acceptedFuzzTypes {
275 acceptedFuzzTypesStrings = append(acceptedFuzzTypesStrings, typ.String())
276 }
277 acceptedFuzzTypesMsg := strings.Join(acceptedFuzzTypesStrings, ", ")
278 return acceptedFuzzTypesMsg
279 }
280
281 func isExampleSuffix(s string) bool {
282 r, size := utf8.DecodeRuneInString(s)
283 return size > 0 && unicode.IsLower(r)
284 }
285
286 func isTestSuffix(name string) bool {
287 if len(name) == 0 {
288
289 return true
290 }
291 r, _ := utf8.DecodeRuneInString(name)
292 return !unicode.IsLower(r)
293 }
294
295 func isTestParam(typ ast.Expr, wantType string) bool {
296 ptr, ok := typ.(*ast.StarExpr)
297 if !ok {
298
299 return false
300 }
301
302
303 if name, ok := ptr.X.(*ast.Ident); ok {
304 return name.Name == wantType
305 }
306 if sel, ok := ptr.X.(*ast.SelectorExpr); ok {
307 return sel.Sel.Name == wantType
308 }
309 return false
310 }
311
312 func lookup(pkg *types.Package, name string) []types.Object {
313 if o := pkg.Scope().Lookup(name); o != nil {
314 return []types.Object{o}
315 }
316
317 var ret []types.Object
318
319
320
321
322
323
324
325 for _, imp := range pkg.Imports() {
326 if obj := imp.Scope().Lookup(name); obj != nil {
327 ret = append(ret, obj)
328 }
329 }
330 return ret
331 }
332
333
334 var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)
335
336 type commentMetadata struct {
337 isOutput bool
338 pos token.Pos
339 }
340
341 func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) {
342 commentsInExample := []commentMetadata{}
343 numOutputs := 0
344
345
346
347 for _, cg := range fileComments {
348 if cg.Pos() < fn.Pos() {
349 continue
350 } else if cg.End() > fn.End() {
351 break
352 }
353
354 isOutput := outputRe.MatchString(cg.Text())
355 if isOutput {
356 numOutputs++
357 }
358
359 commentsInExample = append(commentsInExample, commentMetadata{
360 isOutput: isOutput,
361 pos: cg.Pos(),
362 })
363 }
364
365
366 msg := "output comment block must be the last comment block"
367 if numOutputs > 1 {
368 msg = "there can only be one output comment block per example"
369 }
370
371 for i, cg := range commentsInExample {
372
373 isLast := (i == len(commentsInExample)-1)
374 if cg.isOutput && !isLast {
375 pass.Report(
376 analysis.Diagnostic{
377 Pos: cg.pos,
378 Message: msg,
379 },
380 )
381 }
382 }
383 }
384
385 func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) {
386 fnName := fn.Name.Name
387 if params := fn.Type.Params; len(params.List) != 0 {
388 pass.Reportf(fn.Pos(), "%s should be niladic", fnName)
389 }
390 if results := fn.Type.Results; results != nil && len(results.List) != 0 {
391 pass.Reportf(fn.Pos(), "%s should return nothing", fnName)
392 }
393 if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 {
394 pass.Reportf(fn.Pos(), "%s should not have type params", fnName)
395 }
396
397 if fnName == "Example" {
398
399 return
400 }
401
402 var (
403 exName = strings.TrimPrefix(fnName, "Example")
404 elems = strings.SplitN(exName, "_", 3)
405 ident = elems[0]
406 objs = lookup(pass.Pkg, ident)
407 )
408 if ident != "" && len(objs) == 0 {
409
410 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident)
411
412 return
413 }
414 if len(elems) < 2 {
415
416 return
417 }
418
419 if ident == "" {
420
421 if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) {
422 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual)
423 }
424 return
425 }
426
427 mmbr := elems[1]
428 if !isExampleSuffix(mmbr) {
429
430 found := false
431
432 for _, obj := range objs {
433 if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil {
434 found = true
435 break
436 }
437 }
438 if !found {
439 pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr)
440 }
441 }
442 if len(elems) == 3 && !isExampleSuffix(elems[2]) {
443
444 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2])
445 }
446 }
447
448 func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) {
449
450 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
451 fn.Type.Params == nil ||
452 len(fn.Type.Params.List) != 1 ||
453 len(fn.Type.Params.List[0].Names) > 1 {
454 return
455 }
456
457
458 if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) {
459 return
460 }
461
462 if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 {
463
464
465
466 pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix)
467 }
468
469 if !isTestSuffix(fn.Name.Name[len(prefix):]) {
470
471 pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix)
472 }
473 }
474
View as plain text