diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 334898cd..b8bb50d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,10 @@ jobs: uses: actions/checkout@v2 - name: Init Hermit run: ./bin/hermit env -r >> $GITHUB_ENV - - name: Test + - name: Test Participle run: go test ./... + - name: Test Examples + run: cd ./_examples && go test ./... lint: name: Lint runs-on: ubuntu-latest diff --git a/_examples/go.mod b/_examples/go.mod index 815aac97..3b0c9206 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -5,14 +5,11 @@ go 1.18 require ( github.com/alecthomas/assert/v2 v2.0.3 github.com/alecthomas/go-thrift v0.0.0-20170109061633-7914173639b2 - github.com/alecthomas/kong v0.5.0 - github.com/alecthomas/participle/v2 v2.0.0-alpha1 + github.com/alecthomas/kong v0.6.1 + github.com/alecthomas/participle/v2 v2.0.0-alpha11 github.com/alecthomas/repr v0.1.0 ) -require ( - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/pkg/errors v0.9.1 // indirect -) +require github.com/hexops/gotextdiff v1.0.3 // indirect replace github.com/alecthomas/participle/v2 => ../ diff --git a/_examples/go.sum b/_examples/go.sum index fb6901b6..adb7c0e2 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -2,8 +2,8 @@ github.com/alecthomas/assert/v2 v2.0.3 h1:WKqJODfOiQG0nEJKFKzDIG3E29CN2/4zR9XGJz github.com/alecthomas/assert/v2 v2.0.3/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= github.com/alecthomas/go-thrift v0.0.0-20170109061633-7914173639b2 h1:gKv6LPDhF/G3cNribA+kZtNPiPpKabZGLhcJuEtp3ig= github.com/alecthomas/go-thrift v0.0.0-20170109061633-7914173639b2/go.mod h1:CxCgO+NdpMdi9SsTlGbc0W+/UNxO3I0AabOEJZ3w61w= -github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE= -github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0= +github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA= +github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I= github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= @@ -12,14 +12,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/_examples/thrift/main.go b/_examples/thrift/main.go index 0fa4531f..cb6e77bc 100644 --- a/_examples/thrift/main.go +++ b/_examples/thrift/main.go @@ -227,9 +227,9 @@ func main() { for _, file := range cli.Files { thrift := &Thrift{} r, err := os.Open(file) - ctx.FatalIfError(err, "") + ctx.FatalIfErrorf(err, "") err = parser.Parse("", r, thrift) - ctx.FatalIfError(err, "") + ctx.FatalIfErrorf(err, "") repr.Println(thrift) } } diff --git a/ebnf.go b/ebnf.go index 0b127205..b3b0c027 100644 --- a/ebnf.go +++ b/ebnf.go @@ -9,7 +9,7 @@ import ( // // Productions are always upper cased. Lexer tokens are always lower case. func (p *Parser) String() string { - return ebnf(p.root) + return ebnf(p.typeNodes[p.rootType]) } type ebnfp struct { diff --git a/parser.go b/parser.go index 140cf7f5..289777a0 100644 --- a/parser.go +++ b/parser.go @@ -22,9 +22,9 @@ type customDef struct { // A Parser for a particular grammar and lexer. type Parser struct { - root node lex lexer.Definition - typ reflect.Type + rootType reflect.Type + typeNodes map[reflect.Type]node useLookahead int caseInsensitive map[string]bool mappers []mapperByToken @@ -105,14 +105,16 @@ func Build(grammar interface{}, options ...Option) (parser *Parser, err error) { if v.Kind() == reflect.Interface { v = v.Elem() } - p.typ = v.Type() - p.root, err = context.parseType(p.typ) + p.rootType = v.Type() + rootNode, err := context.parseType(p.rootType) if err != nil { return nil, err } - if err := validate(p.root); err != nil { + if err := validate(rootNode); err != nil { return nil, err } + p.typeNodes = context.typeNodes + p.typeNodes[p.rootType] = rootNode return p, nil } @@ -137,15 +139,9 @@ func (p *Parser) Lex(filename string, r io.Reader) ([]lexer.Token, error) { // This may return a Error. func (p *Parser) ParseFromLexer(lex *lexer.PeekingLexer, v interface{}, options ...ParseOption) error { rv := reflect.ValueOf(v) - if rv.Kind() == reflect.Interface { - rv = rv.Elem() - } - rt := rv.Type() - if rt != p.typ { - return fmt.Errorf("must parse into value of type %s not %T", p.typ, v) - } - if rt.Kind() != reflect.Ptr || rt.Elem().Kind() != reflect.Struct { - return fmt.Errorf("target must be a pointer to a struct, not %s", rt) + parseNode, err := p.parseNodeFor(rv) + if err != nil { + return err } caseInsensitive := map[lexer.TokenType]bool{} for sym, tt := range p.lex.Symbols() { @@ -162,7 +158,7 @@ func (p *Parser) ParseFromLexer(lex *lexer.PeekingLexer, v interface{}, options if parseable, ok := v.(Parseable); ok { return p.rootParseable(ctx, parseable) } - return p.parseOne(ctx, rv) + return p.parseOne(ctx, parseNode, rv) } func (p *Parser) parse(lex lexer.Lexer, v interface{}, options ...ParseOption) (err error) { @@ -222,8 +218,8 @@ func (p *Parser) ParseBytes(filename string, b []byte, v interface{}, options .. return p.parse(lex, v, options...) } -func (p *Parser) parseOne(ctx *parseContext, rv reflect.Value) error { - err := p.parseInto(ctx, rv) +func (p *Parser) parseOne(ctx *parseContext, parseNode node, rv reflect.Value) error { + err := p.parseInto(ctx, parseNode, rv) if err != nil { return err } @@ -234,11 +230,11 @@ func (p *Parser) parseOne(ctx *parseContext, rv reflect.Value) error { return nil } -func (p *Parser) parseInto(ctx *parseContext, rv reflect.Value) error { +func (p *Parser) parseInto(ctx *parseContext, parseNode node, rv reflect.Value) error { if rv.IsNil() { - return fmt.Errorf("target must be a non-nil pointer to a struct, but is a nil %s", rv.Type()) + return fmt.Errorf("target must be a non-nil pointer to a struct or interface, but is a nil %s", rv.Type()) } - pv, err := p.root.Parse(ctx, rv.Elem()) + pv, err := p.typeNodes[rv.Type().Elem()].Parse(ctx, rv.Elem()) if len(pv) > 0 && pv[0].Type() == rv.Elem().Type() { rv.Elem().Set(reflect.Indirect(pv[0])) } @@ -280,3 +276,22 @@ func (p *Parser) getElidedTypes() []lexer.TokenType { } return elideTypes } + +func (p *Parser) parseNodeFor(v reflect.Value) (node, error) { + t := v.Type() + if t.Kind() == reflect.Interface { + t = t.Elem() + } + if t.Kind() != reflect.Ptr || (t.Elem().Kind() != reflect.Struct && t.Elem().Kind() != reflect.Interface) { + return nil, fmt.Errorf("expected a pointer to a struct or interface, but got %s", t) + } + node := p.typeNodes[t] + if node == nil { + t = t.Elem() + node = p.typeNodes[t] + } + if node == nil { + return nil, fmt.Errorf("parser does not know how to parse values of type %s", t) + } + return node, nil +} diff --git a/parser_test.go b/parser_test.go index 4d633c5d..8aba3358 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1850,3 +1850,45 @@ BMember1 = | . BMember2 = "{" TestUnionA "}" . `), parser.String()) } + +func TestParseSubProduction(t *testing.T) { + type ( + ListItem struct { + Number *float64 `(@Int | @Float)` + String *string `| @String` + } + + Grammar struct { + List []ListItem `"[" @@ ("," @@)* "]"` + } + ) + + numberItem := func(n float64) ListItem { return ListItem{Number: &n} } + stringItem := func(s string) ListItem { return ListItem{String: &s} } + + p := mustTestParser(t, &Grammar{}, participle.Unquote()) + + var ( + actual Grammar + expected = Grammar{List: []ListItem{numberItem(1), stringItem("test")}} + ) + + require.NoError(t, p.ParseString("", `[ 1, "test" ]`, &actual)) + require.Equal(t, expected, actual) + + var ( + actualItem ListItem + expectedItem = numberItem(1.234e5) + ) + + require.NoError(t, p.ParseString("", `1.234e5`, &actualItem)) + require.Equal(t, expectedItem, actualItem) + + var ( + actualItem2 ListItem + expectedItem2 = stringItem("\t\ttest\t\t") + ) + + require.NoError(t, p.ParseString("", `"\t\ttest\t\t"`, &actualItem2)) + require.Equal(t, expectedItem2, actualItem2) +}