github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/pattern/parser_test.go (about) 1 package pattern 2 3 import ( 4 "fmt" 5 "go/ast" 6 goparser "go/parser" 7 "go/token" 8 "os" 9 "path/filepath" 10 "reflect" 11 "runtime" 12 "strings" 13 "testing" 14 ) 15 16 func TestParse(t *testing.T) { 17 inputs := []string{ 18 `(Binding "name" _)`, 19 `(Binding "name" _:[])`, 20 `(Binding "name" _:_:[])`, 21 } 22 23 p := Parser{} 24 for _, input := range inputs { 25 if _, err := p.Parse(input); err != nil { 26 t.Errorf("failed to parse %q: %s", input, err) 27 } 28 } 29 } 30 31 func FuzzParse(f *testing.F) { 32 var files []*ast.File 33 fset := token.NewFileSet() 34 35 // Ideally we'd check against as much source code as possible, but that's fairly slow, on the order of 500ms per 36 // pattern when checking against the whole standard library. 37 // 38 // We pick the runtime package in the hopes that it contains the most diverse, and weird, code. 39 filepath.Walk(runtime.GOROOT()+"/src/runtime", func(path string, info os.FileInfo, err error) error { 40 if err != nil { 41 // XXX error handling 42 panic(err) 43 } 44 if !strings.HasSuffix(path, ".go") { 45 return nil 46 } 47 f, err := goparser.ParseFile(fset, path, nil, goparser.SkipObjectResolution) 48 if err != nil { 49 return nil 50 } 51 files = append(files, f) 52 return nil 53 }) 54 55 parse := func(in string, allowTypeInfo bool) (Pattern, bool) { 56 p := Parser{ 57 AllowTypeInfo: allowTypeInfo, 58 } 59 pat, err := p.Parse(string(in)) 60 if err != nil { 61 if strings.Contains(err.Error(), "internal error") { 62 panic(err) 63 } 64 return Pattern{}, false 65 } 66 return pat, true 67 } 68 69 f.Fuzz(func(t *testing.T, in []byte) { 70 defer func() { 71 if err := recover(); err != nil { 72 str := fmt.Sprint(err) 73 if strings.Contains(str, "binding already created:") { 74 // This is an invalid pattern, not a real failure 75 } else { 76 // Re-panic the original panic 77 panic(err) 78 } 79 } 80 }() 81 // Parse twice, once with AllowTypeInfo set to true to exercise the parser, and once with it set to false so we 82 // can actually use it in Match, as we don't have type information available. 83 84 pat, ok := parse(string(in), true) 85 if !ok { 86 return 87 } 88 // Make sure we can turn it back into a string 89 _ = pat.Root.String() 90 91 pat, ok = parse(string(in), false) 92 if !ok { 93 return 94 } 95 // Make sure we can turn it back into a string 96 _ = pat.Root.String() 97 98 // Don't check patterns with too many relevant nodes; it's too expensive 99 if len(pat.Relevant) < 20 { 100 // Make sure trying to match nodes doesn't panic 101 for _, f := range files { 102 ast.Inspect(f, func(node ast.Node) bool { 103 rt := reflect.TypeOf(node) 104 // We'd prefer calling Match on all nodes, not just those the pattern deems relevant, to find more bugs. 105 // However, doing so has a 10x cost in execution time. 106 if _, ok := pat.Relevant[rt]; ok { 107 Match(pat, node) 108 } 109 return true 110 }) 111 } 112 } 113 }) 114 }