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  }