Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/tests/tests.go

     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  	"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/internal/typeparams"
    20  )
    21  
    22  const Doc = `check for common mistaken usages of tests and examples
    23  
    24  The tests checker walks Test, Benchmark and Example functions checking
    25  malformed names, wrong signatures and examples documenting non-existent
    26  identifiers.
    27  
    28  Please see the documentation for package testing in golang.org/pkg/testing
    29  for the conventions that are enforced for Tests, Benchmarks, and Examples.`
    30  
    31  var Analyzer = &analysis.Analyzer{
    32  	Name: "tests",
    33  	Doc:  Doc,
    34  	Run:  run,
    35  }
    36  
    37  func run(pass *analysis.Pass) (interface{}, error) {
    38  	for _, f := range pass.Files {
    39  		if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") {
    40  			continue
    41  		}
    42  		for _, decl := range f.Decls {
    43  			fn, ok := decl.(*ast.FuncDecl)
    44  			if !ok || fn.Recv != nil {
    45  				// Ignore non-functions or functions with receivers.
    46  				continue
    47  			}
    48  			switch {
    49  			case strings.HasPrefix(fn.Name.Name, "Example"):
    50  				checkExampleName(pass, fn)
    51  				checkExampleOutput(pass, fn, f.Comments)
    52  			case strings.HasPrefix(fn.Name.Name, "Test"):
    53  				checkTest(pass, fn, "Test")
    54  			case strings.HasPrefix(fn.Name.Name, "Benchmark"):
    55  				checkTest(pass, fn, "Benchmark")
    56  			}
    57  		}
    58  	}
    59  	return nil, nil
    60  }
    61  
    62  func isExampleSuffix(s string) bool {
    63  	r, size := utf8.DecodeRuneInString(s)
    64  	return size > 0 && unicode.IsLower(r)
    65  }
    66  
    67  func isTestSuffix(name string) bool {
    68  	if len(name) == 0 {
    69  		// "Test" is ok.
    70  		return true
    71  	}
    72  	r, _ := utf8.DecodeRuneInString(name)
    73  	return !unicode.IsLower(r)
    74  }
    75  
    76  func isTestParam(typ ast.Expr, wantType string) bool {
    77  	ptr, ok := typ.(*ast.StarExpr)
    78  	if !ok {
    79  		// Not a pointer.
    80  		return false
    81  	}
    82  	// No easy way of making sure it's a *testing.T or *testing.B:
    83  	// ensure the name of the type matches.
    84  	if name, ok := ptr.X.(*ast.Ident); ok {
    85  		return name.Name == wantType
    86  	}
    87  	if sel, ok := ptr.X.(*ast.SelectorExpr); ok {
    88  		return sel.Sel.Name == wantType
    89  	}
    90  	return false
    91  }
    92  
    93  func lookup(pkg *types.Package, name string) []types.Object {
    94  	if o := pkg.Scope().Lookup(name); o != nil {
    95  		return []types.Object{o}
    96  	}
    97  
    98  	var ret []types.Object
    99  	// Search through the imports to see if any of them define name.
   100  	// It's hard to tell in general which package is being tested, so
   101  	// for the purposes of the analysis, allow the object to appear
   102  	// in any of the imports. This guarantees there are no false positives
   103  	// because the example needs to use the object so it must be defined
   104  	// in the package or one if its imports. On the other hand, false
   105  	// negatives are possible, but should be rare.
   106  	for _, imp := range pkg.Imports() {
   107  		if obj := imp.Scope().Lookup(name); obj != nil {
   108  			ret = append(ret, obj)
   109  		}
   110  	}
   111  	return ret
   112  }
   113  
   114  // This pattern is taken from /go/src/go/doc/example.go
   115  var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)
   116  
   117  type commentMetadata struct {
   118  	isOutput bool
   119  	pos      token.Pos
   120  }
   121  
   122  func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) {
   123  	commentsInExample := []commentMetadata{}
   124  	numOutputs := 0
   125  
   126  	// Find the comment blocks that are in the example. These comments are
   127  	// guaranteed to be in order of appearance.
   128  	for _, cg := range fileComments {
   129  		if cg.Pos() < fn.Pos() {
   130  			continue
   131  		} else if cg.End() > fn.End() {
   132  			break
   133  		}
   134  
   135  		isOutput := outputRe.MatchString(cg.Text())
   136  		if isOutput {
   137  			numOutputs++
   138  		}
   139  
   140  		commentsInExample = append(commentsInExample, commentMetadata{
   141  			isOutput: isOutput,
   142  			pos:      cg.Pos(),
   143  		})
   144  	}
   145  
   146  	// Change message based on whether there are multiple output comment blocks.
   147  	msg := "output comment block must be the last comment block"
   148  	if numOutputs > 1 {
   149  		msg = "there can only be one output comment block per example"
   150  	}
   151  
   152  	for i, cg := range commentsInExample {
   153  		// Check for output comments that are not the last comment in the example.
   154  		isLast := (i == len(commentsInExample)-1)
   155  		if cg.isOutput && !isLast {
   156  			pass.Report(
   157  				analysis.Diagnostic{
   158  					Pos:     cg.pos,
   159  					Message: msg,
   160  				},
   161  			)
   162  		}
   163  	}
   164  }
   165  
   166  func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) {
   167  	fnName := fn.Name.Name
   168  	if params := fn.Type.Params; len(params.List) != 0 {
   169  		pass.Reportf(fn.Pos(), "%s should be niladic", fnName)
   170  	}
   171  	if results := fn.Type.Results; results != nil && len(results.List) != 0 {
   172  		pass.Reportf(fn.Pos(), "%s should return nothing", fnName)
   173  	}
   174  	if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 {
   175  		pass.Reportf(fn.Pos(), "%s should not have type params", fnName)
   176  	}
   177  
   178  	if fnName == "Example" {
   179  		// Nothing more to do.
   180  		return
   181  	}
   182  
   183  	var (
   184  		exName = strings.TrimPrefix(fnName, "Example")
   185  		elems  = strings.SplitN(exName, "_", 3)
   186  		ident  = elems[0]
   187  		objs   = lookup(pass.Pkg, ident)
   188  	)
   189  	if ident != "" && len(objs) == 0 {
   190  		// Check ExampleFoo and ExampleBadFoo.
   191  		pass.Reportf(fn.Pos(), "%s refers to unknown identifier: %s", fnName, ident)
   192  		// Abort since obj is absent and no subsequent checks can be performed.
   193  		return
   194  	}
   195  	if len(elems) < 2 {
   196  		// Nothing more to do.
   197  		return
   198  	}
   199  
   200  	if ident == "" {
   201  		// Check Example_suffix and Example_BadSuffix.
   202  		if residual := strings.TrimPrefix(exName, "_"); !isExampleSuffix(residual) {
   203  			pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, residual)
   204  		}
   205  		return
   206  	}
   207  
   208  	mmbr := elems[1]
   209  	if !isExampleSuffix(mmbr) {
   210  		// Check ExampleFoo_Method and ExampleFoo_BadMethod.
   211  		found := false
   212  		// Check if Foo.Method exists in this package or its imports.
   213  		for _, obj := range objs {
   214  			if obj, _, _ := types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), mmbr); obj != nil {
   215  				found = true
   216  				break
   217  			}
   218  		}
   219  		if !found {
   220  			pass.Reportf(fn.Pos(), "%s refers to unknown field or method: %s.%s", fnName, ident, mmbr)
   221  		}
   222  	}
   223  	if len(elems) == 3 && !isExampleSuffix(elems[2]) {
   224  		// Check ExampleFoo_Method_suffix and ExampleFoo_Method_Badsuffix.
   225  		pass.Reportf(fn.Pos(), "%s has malformed example suffix: %s", fnName, elems[2])
   226  	}
   227  }
   228  
   229  func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) {
   230  	// Want functions with 0 results and 1 parameter.
   231  	if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
   232  		fn.Type.Params == nil ||
   233  		len(fn.Type.Params.List) != 1 ||
   234  		len(fn.Type.Params.List[0].Names) > 1 {
   235  		return
   236  	}
   237  
   238  	// The param must look like a *testing.T or *testing.B.
   239  	if !isTestParam(fn.Type.Params.List[0].Type, prefix[:1]) {
   240  		return
   241  	}
   242  
   243  	if tparams := typeparams.ForFuncType(fn.Type); tparams != nil && len(tparams.List) > 0 {
   244  		// Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters.
   245  		// We have currently decided to also warn before compilation/package loading. This can help users in IDEs.
   246  		pass.Reportf(fn.Pos(), "%s has type parameters: it will not be run by go test as a %sXXX function", fn.Name.Name, prefix)
   247  	}
   248  
   249  	if !isTestSuffix(fn.Name.Name[len(prefix):]) {
   250  		pass.Reportf(fn.Pos(), "%s has malformed name: first letter after '%s' must not be lowercase", fn.Name.Name, prefix)
   251  	}
   252  }
   253  

View as plain text