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  }