GoPLS Viewer

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