Skip to content

Commit 2ff4db7

Browse files
committed
go/analysis/passes/tests: Check malformed fuzz target.
This will validate that first letter after FuzzFoo() should be uppercase Also, following validation will be performed for f.Fuzz() calls : 1. f.Fuzz() should call a function and it should be of type (*testing.F).Fuzz(). 2. The called function in f.Fuzz(func(){}) should not return result. 3. First argument of func() should be of type *testing.T 4. Second argument onwards should be of type []byte, string, bool, byte, rune, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 For golang/go#50198 Change-Id: I540daf635f0fe03d954b010b9b5f8616fd5df47a Reviewed-on: https://go-review.googlesource.com/c/tools/+/374495 Reviewed-by: Robert Findley <[email protected]> Trust: Peter Weinberger <[email protected]>
1 parent 11109f6 commit 2ff4db7

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
package a
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func Fuzzfoo(*testing.F) {} // want "first letter after 'Fuzz' must not be lowercase"
11+
12+
func FuzzBoo(*testing.F) {} // OK because first letter after 'Fuzz' is Uppercase.
13+
14+
func FuzzCallDifferentFunc(f *testing.F) {
15+
f.Name() //OK
16+
}
17+
18+
func FuzzFunc(f *testing.F) {
19+
f.Fuzz(func(t *testing.T) {}) // OK "first argument is of type *testing.T"
20+
}
21+
22+
func FuzzFuncWithArgs(f *testing.F) {
23+
f.Fuzz(func(t *testing.T, i int, b []byte) {}) // OK "arguments in func are allowed"
24+
}
25+
26+
func FuzzArgFunc(f *testing.F) {
27+
f.Fuzz(0) // want "argument to Fuzz must be a function"
28+
}
29+
30+
func FuzzFuncWithReturn(f *testing.F) {
31+
f.Fuzz(func(t *testing.T) bool { return true }) // want "fuzz target must not return any value"
32+
}
33+
34+
func FuzzFuncNoArg(f *testing.F) {
35+
f.Fuzz(func() {}) // want "fuzz target must have 1 or more argument"
36+
}
37+
38+
func FuzzFuncFirstArgNotTesting(f *testing.F) {
39+
f.Fuzz(func(i int64) {}) // want "the first parameter of a fuzz target must be \\*testing.T"
40+
}
41+
42+
func FuzzFuncFirstArgTestingNotT(f *testing.F) {
43+
f.Fuzz(func(t *testing.F) {}) // want "the first parameter of a fuzz target must be \\*testing.T"
44+
}
45+
46+
func FuzzFuncSecondArgNotAllowed(f *testing.F) {
47+
f.Fuzz(func(t *testing.T, i complex64) {}) // want "fuzzing arguments can only have the following types: string, bool, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, \\[\\]byte"
48+
}
49+
50+
func FuzzFuncSecondArgArrNotAllowed(f *testing.F) {
51+
f.Fuzz(func(t *testing.T, i []int) {}) // want "fuzzing arguments can only have the following types: string, bool, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, \\[\\]byte"
52+
}
53+
54+
func FuzzFuncInner(f *testing.F) {
55+
innerFunc := func(t *testing.T, i float32) {}
56+
f.Fuzz(innerFunc) // ok
57+
}
58+
59+
func FuzzArrayOfFunc(f *testing.F) {
60+
var funcs = []func(t *testing.T, i int){func(t *testing.T, i int) {}}
61+
f.Fuzz(funcs[0]) // ok
62+
}
63+
64+
type GenericSlice[T any] []T
65+
66+
func FuzzGenericFunc(f *testing.F) {
67+
g := GenericSlice[func(t *testing.T, i int)]{func(t *testing.T, i int) {}}
68+
f.Fuzz(g[0]) // ok
69+
}
70+
71+
type F func(t *testing.T, i int32)
72+
73+
type myType struct {
74+
myVar F
75+
}
76+
77+
func FuzzObjectMethod(f *testing.F) {
78+
obj := myType{
79+
myVar: func(t *testing.T, i int32) {},
80+
}
81+
f.Fuzz(obj.myVar) // ok
82+
}

go/analysis/passes/tests/tests.go

+133
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"unicode/utf8"
1717

1818
"golang.org/x/tools/go/analysis"
19+
"golang.org/x/tools/internal/analysisinternal"
1920
"golang.org/x/tools/internal/typeparams"
2021
)
2122

@@ -34,6 +35,24 @@ var Analyzer = &analysis.Analyzer{
3435
Run: run,
3536
}
3637

38+
var acceptedFuzzTypes = []types.Type{
39+
types.Typ[types.String],
40+
types.Typ[types.Bool],
41+
types.Typ[types.Float32],
42+
types.Typ[types.Float64],
43+
types.Typ[types.Int],
44+
types.Typ[types.Int8],
45+
types.Typ[types.Int16],
46+
types.Typ[types.Int32],
47+
types.Typ[types.Int64],
48+
types.Typ[types.Uint],
49+
types.Typ[types.Uint8],
50+
types.Typ[types.Uint16],
51+
types.Typ[types.Uint32],
52+
types.Typ[types.Uint64],
53+
types.NewSlice(types.Universe.Lookup("byte").Type()),
54+
}
55+
3756
func run(pass *analysis.Pass) (interface{}, error) {
3857
for _, f := range pass.Files {
3958
if !strings.HasSuffix(pass.Fset.File(f.Pos()).Name(), "_test.go") {
@@ -54,11 +73,125 @@ func run(pass *analysis.Pass) (interface{}, error) {
5473
case strings.HasPrefix(fn.Name.Name, "Benchmark"):
5574
checkTest(pass, fn, "Benchmark")
5675
}
76+
// run fuzz tests diagnostics only for 1.18 i.e. when analysisinternal.DiagnoseFuzzTests is turned on.
77+
if strings.HasPrefix(fn.Name.Name, "Fuzz") && analysisinternal.DiagnoseFuzzTests {
78+
checkTest(pass, fn, "Fuzz")
79+
checkFuzzCall(pass, fn)
80+
}
5781
}
5882
}
5983
return nil, nil
6084
}
6185

86+
// Check the arguments of f.Fuzz() calls :
87+
// 1. f.Fuzz() should call a function and it should be of type (*testing.F).Fuzz().
88+
// 2. The called function in f.Fuzz(func(){}) should not return result.
89+
// 3. First argument of func() should be of type *testing.T
90+
// 4. Second argument onwards should be of type []byte, string, bool, byte,
91+
// rune, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16,
92+
// uint32, uint64
93+
func checkFuzzCall(pass *analysis.Pass, fn *ast.FuncDecl) {
94+
ast.Inspect(fn, func(n ast.Node) bool {
95+
call, ok := n.(*ast.CallExpr)
96+
if ok {
97+
if !isFuzzTargetDotFuzz(pass, call) {
98+
return true
99+
}
100+
101+
// Only one argument (func) must be passed to (*testing.F).Fuzz.
102+
if len(call.Args) != 1 {
103+
return true
104+
}
105+
expr := call.Args[0]
106+
if pass.TypesInfo.Types[expr].Type == nil {
107+
return true
108+
}
109+
t := pass.TypesInfo.Types[expr].Type.Underlying()
110+
tSign, argOk := t.(*types.Signature)
111+
// Argument should be a function
112+
if !argOk {
113+
pass.ReportRangef(expr, "argument to Fuzz must be a function")
114+
return true
115+
}
116+
// ff Argument function should not return
117+
if tSign.Results().Len() != 0 {
118+
pass.ReportRangef(expr, "fuzz target must not return any value")
119+
}
120+
// ff Argument function should have 1 or more argument
121+
if tSign.Params().Len() == 0 {
122+
pass.ReportRangef(expr, "fuzz target must have 1 or more argument")
123+
return true
124+
}
125+
validateFuzzArgs(pass, tSign.Params(), expr)
126+
}
127+
return true
128+
})
129+
}
130+
131+
// isFuzzTargetDotFuzz reports whether call is (*testing.F).Fuzz().
132+
func isFuzzTargetDotFuzz(pass *analysis.Pass, call *ast.CallExpr) bool {
133+
if selExpr, ok := call.Fun.(*ast.SelectorExpr); ok {
134+
if !isTestingType(pass.TypesInfo.Types[selExpr.X].Type, "F") {
135+
return false
136+
}
137+
if selExpr.Sel.Name == "Fuzz" {
138+
return true
139+
}
140+
}
141+
return false
142+
}
143+
144+
// Validate the arguments of fuzz target.
145+
func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) {
146+
fLit, isFuncLit := expr.(*ast.FuncLit)
147+
exprRange := expr
148+
if !isTestingType(params.At(0).Type(), "T") {
149+
if isFuncLit {
150+
exprRange = fLit.Type.Params.List[0].Type
151+
}
152+
pass.ReportRangef(exprRange, "the first parameter of a fuzz target must be *testing.T")
153+
}
154+
for i := 1; i < params.Len(); i++ {
155+
if !isAcceptedFuzzType(params.At(i).Type()) {
156+
if isFuncLit {
157+
exprRange = fLit.Type.Params.List[i].Type
158+
}
159+
pass.ReportRangef(exprRange, "fuzzing arguments can only have the following types: "+printAcceptedFuzzType())
160+
}
161+
}
162+
}
163+
164+
func isTestingType(typ types.Type, testingType string) bool {
165+
ptr, ok := typ.(*types.Pointer)
166+
if !ok {
167+
return false
168+
}
169+
named, ok := ptr.Elem().(*types.Named)
170+
if !ok {
171+
return false
172+
}
173+
return named.Obj().Pkg().Path() == "testing" && named.Obj().Name() == testingType
174+
}
175+
176+
// Validate that fuzz target function's arguments are of accepted types.
177+
func isAcceptedFuzzType(paramType types.Type) bool {
178+
for _, typ := range acceptedFuzzTypes {
179+
if types.Identical(typ, paramType) {
180+
return true
181+
}
182+
}
183+
return false
184+
}
185+
186+
func formatAcceptedFuzzType() string {
187+
var acceptedFuzzTypesStrings []string
188+
for _, typ := range acceptedFuzzTypes {
189+
acceptedFuzzTypesStrings = append(acceptedFuzzTypesStrings, typ.String())
190+
}
191+
acceptedFuzzTypesMsg := strings.Join(acceptedFuzzTypesStrings, ", ")
192+
return acceptedFuzzTypesMsg
193+
}
194+
62195
func isExampleSuffix(s string) bool {
63196
r, size := utf8.DecodeRuneInString(s)
64197
return size > 0 && unicode.IsLower(r)

go/analysis/passes/tests/tests_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@ package tests_test
77
import (
88
"testing"
99

10+
"golang.org/x/tools/internal/analysisinternal"
11+
1012
"golang.org/x/tools/go/analysis/analysistest"
1113
"golang.org/x/tools/go/analysis/passes/tests"
1214
"golang.org/x/tools/internal/typeparams"
1315
)
1416

1517
func Test(t *testing.T) {
18+
// In 1.18, diagnostic for Fuzz Tests must not be used by cmd/vet.
19+
// So the code for Fuzz tests diagnostics is guarded behind flag analysisinternal.DiagnoseFuzzTests
20+
// Turn on the flag DiagnoseFuzzTests for analysis tests and then turn it off.
21+
analysisinternal.DiagnoseFuzzTests = true
22+
defer func() {
23+
analysisinternal.DiagnoseFuzzTests = false
24+
}()
1625
testdata := analysistest.TestData()
1726
pkgs := []string{
1827
"a", // loads "a", "a [a.test]", and "a.test"

gopls/main.go

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package main // import "golang.org/x/tools/gopls"
1313

1414
import (
1515
"context"
16+
"golang.org/x/tools/internal/analysisinternal"
1617
"os"
1718

1819
"golang.org/x/tools/gopls/internal/hooks"
@@ -21,6 +22,10 @@ import (
2122
)
2223

2324
func main() {
25+
// In 1.18, diagnostics for Fuzz tests must not be used by cmd/vet.
26+
// So the code for Fuzz tests diagnostics is guarded behind flag analysisinternal.DiagnoseFuzzTests
27+
// Turn on analysisinternal.DiagnoseFuzzTests for gopls
28+
analysisinternal.DiagnoseFuzzTests = true
2429
ctx := context.Background()
2530
tool.Main(ctx, cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
2631
}

internal/analysisinternal/analysis.go

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import (
1717
"golang.org/x/tools/internal/lsp/fuzzy"
1818
)
1919

20+
// Flag to gate diagnostics for fuzz tests in 1.18.
21+
var DiagnoseFuzzTests bool = false
22+
2023
var (
2124
GetTypeErrors func(p interface{}) []types.Error
2225
SetTypeErrors func(p interface{}, errors []types.Error)

0 commit comments

Comments
 (0)