From c85e6aacf48e19f3cf5efdfbad9fd87a0e9be4f2 Mon Sep 17 00:00:00 2001 From: mido Date: Thu, 25 Apr 2024 08:00:38 -0700 Subject: [PATCH 1/3] Implement Chaining function --- ast/call_expression.go | 11 +++--- compiler.go | 28 ++++++++++++-- parser/parser.go | 15 ++++++++ struct_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 9 deletions(-) diff --git a/ast/call_expression.go b/ast/call_expression.go index 76c0eae..c940d0b 100644 --- a/ast/call_expression.go +++ b/ast/call_expression.go @@ -7,11 +7,12 @@ import ( type CallExpression struct { TokenAble - Callee Expression - Function Expression - Arguments []Expression - Block *BlockStatement - ElseBlock *BlockStatement + Callee Expression + ChainCallee Expression + Function Expression + Arguments []Expression + Block *BlockStatement + ElseBlock *BlockStatement } var _ Comparable = &CallExpression{} diff --git a/compiler.go b/compiler.go index bec2b57..7150a07 100644 --- a/compiler.go +++ b/compiler.go @@ -3,6 +3,7 @@ package plush import ( "bytes" "fmt" + "log" "github.com/gobuffalo/plush/v4/token" @@ -398,7 +399,7 @@ func (c *compiler) evalIdentifier(node *ast.Identifier) (interface{}, error) { if rv.Kind() == reflect.Ptr { rv = rv.Elem() } - + log.Println("FUND ME NSNSNS") if rv.Kind() != reflect.Struct { return nil, fmt.Errorf("'%s' does not have a field or method named '%s' (%s)", node.Callee.String(), node.Value, node) } @@ -605,13 +606,11 @@ func (c *compiler) stringsOperator(l string, r interface{}, op string) (interfac func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, error) { var rv reflect.Value - if node.Callee != nil { c, err := c.evalExpression(node.Callee) if err != nil { return nil, err } - rc := reflect.ValueOf(c) mname := node.Function.String() if i, ok := node.Function.(*ast.Identifier); ok { @@ -623,6 +622,9 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er ptr := reflect.New(reflect.TypeOf(c)) ptr.Elem().Set(rc) rv = ptr.MethodByName(mname) + if !rv.IsValid() { + return nil, fmt.Errorf("'%s' does not have a method named '%s' (%s.%s)", node.Callee.String(), mname, node.Callee.String(), mname) + } } if !rv.IsValid() { @@ -635,6 +637,7 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er return rc.Interface(), nil } + } else { f, err := c.evalExpression(node.Function) if err != nil { @@ -660,7 +663,7 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er if rt.Kind() != reflect.Func { return nil, fmt.Errorf("%+v (%T) is an invalid function", node.String(), rt) } - + log.Println("NONANANA") rtNumIn := rt.NumIn() isVariadic := rt.IsVariadic() args := []reflect.Value{} @@ -800,6 +803,23 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er if e, ok := res[len(res)-1].Interface().(error); ok { return nil, fmt.Errorf("could not call %s function: %w", node.Function, e) } + if node.ChainCallee != nil { + octx := c.ctx.(*Context) + defer func() { + c.ctx = octx + }() + + c.ctx = octx.New() + for k, v := range octx.data { + c.ctx.Set(k, v) + } + c.ctx.Set(node.Function.String(), res[0].Interface()) + vvs, err := c.evalExpression(node.ChainCallee) + if err != nil { + return nil, err + } + return vvs, err + } return res[0].Interface(), nil } diff --git a/parser/parser.go b/parser/parser.go index 066b5aa..b72051e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -670,6 +670,18 @@ func (p *parser) parseCallExpression(function ast.Expression) ast.Expression { exp.Block = p.parseBlockStatement() } + if p.peekTokenIs(token.DOT) { + calleeIdent := &ast.Identifier{Value: exp.Function.String()} + p.nextToken() + p.nextToken() + parseExp := p.parseExpression(LOWEST) + + exp.ChainCallee = p.assignCallee(parseExp, calleeIdent) + if exp.ChainCallee == nil { + return nil + } + } + return exp } @@ -749,6 +761,9 @@ func (p *parser) assignCallee(exp ast.Expression, calleeIdent *ast.Identifier) ( msg := fmt.Sprintf("line %d: syntax error: invalid nested index access, expected an identifier %v", p.curToken.LineNumber, ss) p.errors = append(p.errors, msg) } + case *ast.CallExpression: + ss.Callee = calleeIdent + assignedCallee = ss case *ast.Identifier: ss.OriginalCallee.Callee = calleeIdent assignedCallee = ss diff --git a/struct_test.go b/struct_test.go index b29e207..572e4c2 100644 --- a/struct_test.go +++ b/struct_test.go @@ -3,6 +3,7 @@ package plush_test import ( "strings" "testing" + "time" "github.com/gobuffalo/plush/v4" "github.com/stretchr/testify/require" @@ -461,3 +462,85 @@ func Test_Render_Struct_Nested_Map_Access(t *testing.T) { r.NoError(err) r.Equal("John Dolittle", res) } + +type person struct { + likes []string + hates []string + born time.Time +} + +func (a person) GetAge() time.Duration { + return time.Since(a.born) +} + +func (a person) GetBorn() time.Time { + return a.born +} + +func (a person) Hates() []string { + return a.hates +} +func (a person) Likes() []string { + return a.likes +} + +func Test_Render_Struct_With_ChainingFunction_ArrayAccess(t *testing.T) { + r := require.New(t) + + tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold braclet"}, + hates: []string{"boiled eggs", "coconut"}} + input := `<%= nour.Likes()[0] %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + res, err := plush.Render(input, ctx) + r.NoError(err) + r.Equal("pringles", res) +} + +func Test_Render_Struct_With_ChainingFunction_ArrayAccess_Outofbound(t *testing.T) { + r := require.New(t) + + tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold bracelet"}, + hates: []string{"boiled eggs", "coconut"}} + input := `<%= nour.Hates()[30] %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) +} + +func Test_Render_Struct_With_ChainingFunction_FunctionCall(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Date(2024, time.January, 11, 0, 0, 0, 0, time.UTC).AddDate(-31, 0, 0)} + input := `<%= nour.GetBorn().Format("Jan 2, 2006") %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + res, err := plush.Render(input, ctx) + r.NoError(err) + r.Equal("Jan 11, 1993", res) +} + +func Test_Render_Struct_With_ChainingFunction_UndefinedStructProperty(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Now()} + input := `<%= nour.GetBorn().TEST %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) + +} + +func Test_Render_Struct_With_ChainingFunction_InvalidFunctionCall(t *testing.T) { + r := require.New(t) + + tt := person{born: time.Now()} + input := `<%= nour.GetBorn().TEST("Jan 2, 2006") %>` + ctx := plush.NewContext() + ctx.Set("nour", tt) + _, err := plush.Render(input, ctx) + r.Error(err) + r.Contains(err.Error(), "'nour.GetBorn' does not have a method named 'TEST' (nour.GetBorn.TEST)") +} From 7dd2132d79c6a52b221d54a9bd51f880204460d2 Mon Sep 17 00:00:00 2001 From: mido Date: Thu, 25 Apr 2024 08:13:29 -0700 Subject: [PATCH 2/3] Add more test cases --- struct_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/struct_test.go b/struct_test.go index 572e4c2..a85ff1e 100644 --- a/struct_test.go +++ b/struct_test.go @@ -544,3 +544,15 @@ func Test_Render_Struct_With_ChainingFunction_InvalidFunctionCall(t *testing.T) r.Error(err) r.Contains(err.Error(), "'nour.GetBorn' does not have a method named 'TEST' (nour.GetBorn.TEST)") } + +func Test_Render_Function_on_Invalid_Function_Struct(t *testing.T) { + r := require.New(t) + ctx := plush.NewContext() + bender := Robot{ + Avatar: Avatar("bender.jpg"), + } + ctx.Set("robot", bender) + input := `<%= robot.Avatar.URL2() %>` + _, err := plush.Render(input, ctx) + r.Error(err) +} From 5ec33edcd93e6a9f9427bfcfd960a8e9988fdd48 Mon Sep 17 00:00:00 2001 From: mido Date: Thu, 25 Apr 2024 08:16:58 -0700 Subject: [PATCH 3/3] remove debug code --- compiler.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compiler.go b/compiler.go index 7150a07..e1167ad 100644 --- a/compiler.go +++ b/compiler.go @@ -3,7 +3,6 @@ package plush import ( "bytes" "fmt" - "log" "github.com/gobuffalo/plush/v4/token" @@ -399,7 +398,7 @@ func (c *compiler) evalIdentifier(node *ast.Identifier) (interface{}, error) { if rv.Kind() == reflect.Ptr { rv = rv.Elem() } - log.Println("FUND ME NSNSNS") + if rv.Kind() != reflect.Struct { return nil, fmt.Errorf("'%s' does not have a field or method named '%s' (%s)", node.Callee.String(), node.Value, node) } @@ -606,11 +605,13 @@ func (c *compiler) stringsOperator(l string, r interface{}, op string) (interfac func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, error) { var rv reflect.Value + if node.Callee != nil { c, err := c.evalExpression(node.Callee) if err != nil { return nil, err } + rc := reflect.ValueOf(c) mname := node.Function.String() if i, ok := node.Function.(*ast.Identifier); ok { @@ -637,7 +638,6 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er return rc.Interface(), nil } - } else { f, err := c.evalExpression(node.Function) if err != nil { @@ -663,7 +663,6 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er if rt.Kind() != reflect.Func { return nil, fmt.Errorf("%+v (%T) is an invalid function", node.String(), rt) } - log.Println("NONANANA") rtNumIn := rt.NumIn() isVariadic := rt.IsVariadic() args := []reflect.Value{}