honnef.co/go/tools@v0.5.0-0.dev.0.20240520180541-dcae280a5e87/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 "honnef.co/go/tools/debug" 16 ) 17 18 func TestParse(t *testing.T) { 19 inputs := []string{ 20 `(Binding "name" _)`, 21 `(Binding "name" _:[])`, 22 `(Binding "name" _:_:[])`, 23 } 24 25 p := Parser{} 26 for _, input := range inputs { 27 if _, err := p.Parse(input); err != nil { 28 t.Errorf("failed to parse %q: %s", input, err) 29 } 30 } 31 } 32 33 func FuzzParse(f *testing.F) { 34 var files []*ast.File 35 fset := token.NewFileSet() 36 37 // Ideally we'd check against as much source code as possible, but that's fairly slow, on the order of 500ms per 38 // pattern when checking against the whole standard library. 39 // 40 // We pick the runtime package in the hopes that it contains the most diverse, and weird, code. 41 filepath.Walk(runtime.GOROOT()+"/src/runtime", func(path string, info os.FileInfo, err error) error { 42 if err != nil { 43 // XXX error handling 44 panic(err) 45 } 46 if !strings.HasSuffix(path, ".go") { 47 return nil 48 } 49 f, err := goparser.ParseFile(fset, path, nil, goparser.SkipObjectResolution) 50 if err != nil { 51 return nil 52 } 53 files = append(files, f) 54 return nil 55 }) 56 57 parse := func(in string, allowTypeInfo bool) (Pattern, bool) { 58 p := Parser{ 59 AllowTypeInfo: allowTypeInfo, 60 } 61 pat, err := p.Parse(string(in)) 62 if err != nil { 63 if strings.Contains(err.Error(), "internal error") { 64 panic(err) 65 } 66 return Pattern{}, false 67 } 68 return pat, true 69 } 70 71 f.Fuzz(func(t *testing.T, in []byte) { 72 defer func() { 73 if err := recover(); err != nil { 74 str := fmt.Sprint(err) 75 if strings.Contains(str, "binding already created:") { 76 // This is an invalid pattern, not a real failure 77 } else { 78 // Re-panic the original panic 79 panic(err) 80 } 81 } 82 }() 83 // Parse twice, once with AllowTypeInfo set to true to exercise the parser, and once with it set to false so we 84 // can actually use it in Match, as we don't have type information available. 85 86 pat, ok := parse(string(in), true) 87 if !ok { 88 return 89 } 90 // Make sure we can turn it back into a string 91 _ = pat.Root.String() 92 93 pat, ok = parse(string(in), false) 94 if !ok { 95 return 96 } 97 // Make sure we can turn it back into a string 98 _ = pat.Root.String() 99 100 // Don't check patterns with too many relevant nodes; it's too expensive 101 if len(pat.Relevant) < 20 { 102 // Make sure trying to match nodes doesn't panic 103 for _, f := range files { 104 ast.Inspect(f, func(node ast.Node) bool { 105 rt := reflect.TypeOf(node) 106 // We'd prefer calling Match on all nodes, not just those the pattern deems relevant, to find more bugs. 107 // However, doing so has a 10x cost in execution time. 108 if _, ok := pat.Relevant[rt]; ok { 109 Match(pat, node) 110 } 111 return true 112 }) 113 } 114 } 115 }) 116 } 117 118 func TestMatchAlias(t *testing.T) { 119 p1 := MustParse(`(CallExpr (Symbol "foo.Alias") _)`) 120 p2 := MustParse(`(CallExpr (Symbol "int") _)`) 121 122 f, _, info, err := debug.TypeCheck(` 123 package pkg 124 type Alias = int 125 func _() { _ = Alias(0) } 126 `) 127 if err != nil { 128 t.Fatal(err) 129 } 130 131 m := &Matcher{ 132 TypesInfo: info, 133 } 134 node := f.Decls[1].(*ast.FuncDecl).Body.List[0].(*ast.AssignStmt).Rhs[0] 135 136 if debug.AliasesEnabled() { 137 // Check that we can match on the name of the alias 138 if ok := m.Match(p1, node); !ok { 139 t.Errorf("%s did not match", p1.Root) 140 } 141 } 142 143 // Check that we can match on the name of the alias's target 144 if ok := m.Match(p2, node); !ok { 145 t.Errorf("%s did not match", p2.Root) 146 } 147 }