golang.org/x/tools@v0.21.0/go/analysis/passes/tests/tests.go (about) 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 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 //go:embed doc.go 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 // Ignore non-functions or functions with receivers. 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 // checkFuzz checks the contents of a fuzz function. 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 // checkFuzzCall checks the arguments of f.Fuzz() calls: 87 // 88 // 1. f.Fuzz() should call a function and it should be of type (*testing.F).Fuzz(). 89 // 2. The called function in f.Fuzz(func(){}) should not return result. 90 // 3. First argument of func() should be of type *testing.T 91 // 4. Second argument onwards should be of type []byte, string, bool, byte, 92 // rune, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, 93 // uint32, uint64 94 // 5. func() must not call any *F methods, e.g. (*F).Log, (*F).Error, (*F).Skip 95 // The only *F methods that are allowed in the (*F).Fuzz function are (*F).Failed and (*F).Name. 96 // 97 // Returns the list of parameters to the fuzz function, if they are valid fuzz parameters. 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 // Only one argument (func) must be passed to (*testing.F).Fuzz. 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 // Argument should be a function 117 if !argOk { 118 pass.ReportRangef(expr, "argument to Fuzz must be a function") 119 return false 120 } 121 // ff Argument function should not return 122 if tSign.Results().Len() != 0 { 123 pass.ReportRangef(expr, "fuzz target must not return any value") 124 } 125 // ff Argument function should have 1 or more argument 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 // Inspect the function that was passed as an argument to make sure that 135 // there are no calls to *F methods, except for Name and Failed. 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 // We do not need to look at any calls to f.Fuzz inside of a Fuzz call, 148 // since they are not allowed. 149 return false 150 } 151 return true 152 }) 153 return params 154 } 155 156 // checkAddCalls checks that the arguments of f.Add calls have the same number and type of arguments as 157 // the signature of the function passed to (*testing.F).Fuzz 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 // The first argument to function passed to (*testing.F).Fuzz is (*testing.T). 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 // If just one of the types is mismatched report for that 182 // type only. Otherwise report for the whole call to (*testing.F).Add 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 // isFuzzTargetDotFuzz reports whether call is (*testing.F).Fuzz(). 201 func isFuzzTargetDotFuzz(pass *analysis.Pass, call *ast.CallExpr) bool { 202 return isFuzzTargetDot(pass, call, "Fuzz") 203 } 204 205 // isFuzzTargetDotAdd reports whether call is (*testing.F).Add(). 206 func isFuzzTargetDotAdd(pass *analysis.Pass, call *ast.CallExpr) bool { 207 return isFuzzTargetDot(pass, call, "Add") 208 } 209 210 // isFuzzTargetDot reports whether call is (*testing.F).<name>(). 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 // Validate the arguments of fuzz target. 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 // No Unalias here: I doubt "go test" recognizes 256 // "type A = *testing.T; func Test(A) {}" as a test. 257 ptr, ok := typ.(*types.Pointer) 258 if !ok { 259 return false 260 } 261 return analysisutil.IsNamedType(ptr.Elem(), "testing", testingType) 262 } 263 264 // Validate that fuzz target function's arguments are of accepted types. 265 func isAcceptedFuzzType(paramType types.Type) bool { 266 for _, typ := range acceptedFuzzTypes { 267 if types.Identical(typ, paramType) { 268 return true 269 } 270 } 271 return false 272 } 273 274 func formatAcceptedFuzzType() string { 275 var acceptedFuzzTypesStrings []string 276 for _, typ := range acceptedFuzzTypes { 277 acceptedFuzzTypesStrings = append(acceptedFuzzTypesStrings, typ.String()) 278 } 279 acceptedFuzzTypesMsg := strings.Join(acceptedFuzzTypesStrings, ", ") 280 return acceptedFuzzTypesMsg 281 } 282 283 func isExampleSuffix(s string) bool { 284 r, size := utf8.DecodeRuneInString(s) 285 return size > 0 && unicode.IsLower(r) 286 } 287 288 func isTestSuffix(name string) bool { 289 if len(name) == 0 { 290 // "Test" is ok. 291 return true 292 } 293 r, _ := utf8.DecodeRuneInString(name) 294 return !unicode.IsLower(r) 295 } 296 297 func isTestParam(typ ast.Expr, wantType string) bool { 298 ptr, ok := typ.(*ast.StarExpr) 299 if !ok { 300 // Not a pointer. 301 return false 302 } 303 // No easy way of making sure it's a *testing.T or *testing.B: 304 // ensure the name of the type matches. 305 if name, ok := ptr.X.(*ast.Ident); ok { 306 return name.Name == wantType 307 } 308 if sel, ok := ptr.X.(*ast.SelectorExpr); ok { 309 return sel.Sel.Name == wantType 310 } 311 return false 312 } 313 314 func lookup(pkg *types.Package, name string) []types.Object { 315 if o := pkg.Scope().Lookup(name); o != nil { 316 return []types.Object{o} 317 } 318 319 var ret []types.Object 320 // Search through the imports to see if any of them define name. 321 // It's hard to tell in general which package is being tested, so 322 // for the purposes of the analysis, allow the object to appear 323 // in any of the imports. This guarantees there are no false positives 324 // because the example needs to use the object so it must be defined 325 // in the package or one if its imports. On the other hand, false 326 // negatives are possible, but should be rare. 327 for _, imp := range pkg.Imports() { 328 if obj := imp.Scope().Lookup(name); obj != nil { 329 ret = append(ret, obj) 330 } 331 } 332 return ret 333 } 334 335 // This pattern is taken from /go/src/go/doc/example.go 336 var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`) 337 338 type commentMetadata struct { 339 isOutput bool 340 pos token.Pos 341 } 342 343 func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) { 344 commentsInExample := []commentMetadata{} 345 numOutputs := 0 346 347 // Find the comment blocks that are in the example. These comments are 348 // guaranteed to be in order of appearance. 349 for _, cg := range fileComments { 350 if cg.Pos() < fn.Pos() { 351 continue 352 } else if cg.End() > fn.End() { 353 break 354 } 355 356 isOutput := outputRe.MatchString(cg.Text()) 357 if isOutput { 358 numOutputs++ 359 } 360 361 commentsInExample = append(commentsInExample, commentMetadata{ 362 isOutput: isOutput, 363 pos: cg.Pos(), 364 }) 365 } 366 367 // Change message based on whether there are multiple output comment blocks. 368 msg := "output comment block must be the last comment block" 369 if numOutputs > 1 { 370 msg = "there can only be one output comment block per example" 371 } 372 373 for i, cg := range commentsInExample { 374 // Check for output comments that are not the last comment in the example. 375 isLast := (i == len(commentsInExample)-1) 376 if cg.isOutput && !isLast { 377 pass.Report( 378 analysis.Diagnostic{ 379 Pos: cg.pos, 380 Message: msg, 381 }, 382 ) 383 } 384 } 385 } 386 387 func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) { 388 fnName := fn.Name.Name 389 if params := fn.Type.Params; len(params.List) != 0 { 390 pass.Reportf(fn.Pos(), "%s should be niladic", fnName) 391 } 392 if results := fn.Type.Results; results != nil && len(results.List) != 0 { 393 pass.Reportf(fn.Pos(), "%s should return nothing", fnName) 394 } 395 if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 { 396 pass.Reportf(fn.Pos(), "%s should not have type params", fnName) 397 } 398 399 if fnName == "Example" { 400 // Nothing more to do. 401 return 402 } 403 404 var ( 405 exName = strings.TrimPrefix(fnName, "Example") 406 elems = strings.SplitN(exName, "_", 3) 407 ident = elems[0] 408 objs = lookup(pass.Pkg, ident) 409 ) 410 if ident != "" && len(objs) == 0 { 411 // Check ExampleFoo and ExampleBadFoo. 412 pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident) 413 // Abort since obj is absent and no subsequent checks can be performed. 414 return 415 } 416 if len(elems) < 2 { 417 // Nothing more to do. 418 return 419 } 420 421 if ident == "" { 422 // Check Example_suffix and Example_BadSuffix. 423 if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) { 424 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual) 425 } 426 return 427 } 428 429 mmbr := elems[1] 430 if !isExampleSuffix(mmbr) { 431 // Check ExampleFoo_Method and ExampleFoo_BadMethod. 432 found := false 433 // Check if Foo.Method exists in this package or its imports. 434 for _, obj := range objs { 435 if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil { 436 found = true 437 break 438 } 439 } 440 if !found { 441 pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr) 442 } 443 } 444 if len(elems) == 3 && !isExampleSuffix(elems[2]) { 445 // Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix. 446 pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2]) 447 } 448 } 449 450 type tokenRange struct { 451 p, e token.Pos 452 } 453 454 func (r tokenRange) Pos() token.Pos { 455 return r.p 456 } 457 458 func (r tokenRange) End() token.Pos { 459 return r.e 460 } 461 462 func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) { 463 // Want functions with 0 results and 1 parameter. 464 if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 || 465 fn.Type.Params == nil || 466 len(fn.Type.Params.List) != 1 || 467 len(fn.Type.Params.List[0].Names) > 1 { 468 return 469 } 470 471 // The param must look like a *testing.T or *testing.B. 472 if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) { 473 return 474 } 475 476 if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 { 477 // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. 478 // We have currently decided to also warn before compilation/package loading. This can help users in IDEs. 479 at := tokenRange{tparams.Opening, tparams.Closing} 480 pass.ReportRangef(at, "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix) 481 } 482 483 if !isTestSuffix(fn.Name.Name[len(prefix):]) { 484 pass.ReportRangef(fn.Name, "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix) 485 } 486 }