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