From 77eab968b27aee5a711e45495e7b3751a57dec83 Mon Sep 17 00:00:00 2001 From: Anthony Bullard Date: Mon, 15 Jan 2024 09:12:06 -0600 Subject: [PATCH] feat(compiler): support for safe expressions Support for `@(some.expression)` and `@!(some.expression)`(escaped) expressions. - Added test coverage for parsing expressions - Added support for safe expressions - Added test coverage for parsing safe expressions - Updated lsp to highlight safe expressions correctly --- gwirl-lsp/template_utils.go | 9 ++- internal/parser/nodesv2.go | 15 ++++ internal/parser/parse_expressions_test.go | 68 ++++++++++++++++ internal/parser/parse_safe_expression_test.go | 26 ++++++ internal/parser/parserv2.go | 31 +++++++- internal/parser/testdata/testAll.html.gwirl | 1 + internal/parser/testing_fixtures_test.go | 79 +++++++++++++++++++ 7 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 internal/parser/parse_expressions_test.go create mode 100644 internal/parser/parse_safe_expression_test.go create mode 100644 internal/parser/testing_fixtures_test.go diff --git a/gwirl-lsp/template_utils.go b/gwirl-lsp/template_utils.go index 4bd6100..77b0be6 100644 --- a/gwirl-lsp/template_utils.go +++ b/gwirl-lsp/template_utils.go @@ -251,13 +251,20 @@ func absTokensForContent(tt []parser.TemplateTree2) []absToken { case parser.TT2GoExp: length := len(t.Text) var atToken absToken - if t.Metadata.Has(parser.TTMDEscape) { + if t.Metadata.Has(parser.TTMDSafe) && !t.Metadata.Has(parser.TTMDEscape) { + atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator) + } else if t.Metadata.Has(parser.TTMDEscape) && t.Metadata.Has(parser.TTMDSafe) { + atToken = NewAbsToken(startLine, startCol-3, 3, lsp.SemanticTokenOperator) + } else if t.Metadata.Has(parser.TTMDEscape) { atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator) } else { atToken = NewAbsToken(startLine, startCol-1, 1, lsp.SemanticTokenOperator) } token := NewAbsToken(startLine, startCol, length, lsp.SemanticTokenParameter) tokens = append(tokens, atToken, token) + if t.Metadata.Has(parser.TTMDSafe) { + tokens = append(tokens, NewAbsToken(startLine, startCol + uint32(length), 1, lsp.SemanticTokenOperator)) + } if t.Children == nil { continue } diff --git a/internal/parser/nodesv2.go b/internal/parser/nodesv2.go index 922ac04..c44c824 100644 --- a/internal/parser/nodesv2.go +++ b/internal/parser/nodesv2.go @@ -99,6 +99,7 @@ type MetadataFlag int const ( TTMDEscape MetadataFlag = 1 << iota + TTMDSafe = 2 ) func (f MetadataFlag) Has(flag MetadataFlag) bool { return f&flag != 0 } @@ -188,6 +189,20 @@ func NewTT2GoExp(content string, escape bool, transclusions [][]TemplateTree2) T } } +func NewTT2GoExpSafe(content string, escape bool) TemplateTree2 { + var metadata MetadataFlag + metadata.Set(TTMDSafe) + if escape { + metadata.Set(TTMDEscape) + } + return TemplateTree2{ + Type: TT2GoExp, + Text: content, + Metadata: metadata, + Children: [][]TemplateTree2{}, + } +} + func NewTT2BlockComment(content string) TemplateTree2 { return TemplateTree2{ Type: TT2BlockComment, diff --git a/internal/parser/parse_expressions_test.go b/internal/parser/parse_expressions_test.go new file mode 100644 index 0000000..89a04ce --- /dev/null +++ b/internal/parser/parse_expressions_test.go @@ -0,0 +1,68 @@ +package parser_test + +import ( + "testing" + + "github.com/gamebox/gwirl/internal/parser" +) + +var expressionTests = []ParsingTest{ + {"simple expression", "@foobar\"", parser.NewTT2GoExp("foobar", false, noChildren)}, + {"simple method", "@foobar()\"", parser.NewTT2GoExp("foobar()", false, noChildren)}, + {"complex expression", "@foo.bar\"", parser.NewTT2GoExp("foo.bar", false, noChildren)}, + {"complex method", "@foo.bar()\"", parser.NewTT2GoExp("foo.bar()", false, noChildren)}, + {"complex method with params", "@foo.bar(param1, param2)\"", parser.NewTT2GoExp("foo.bar(param1, param2)", false, noChildren)}, + {"complex method with literal params", "@foo.bar(\"hello\", 123)\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123)", false, noChildren)}, + {"complex method with struct literal param", "@foo.bar(MyStruct{something, else}, 123)\"", parser.NewTT2GoExp("foo.bar(MyStruct{something, else}, 123)", false, noChildren)}, + {"complex method with chaining", "@foo.bar().something.else\"", parser.NewTT2GoExp("foo.bar().something.else", false, noChildren)}, + {"complex method with params with chaining", "@foo.bar(param1, param2).something.else\"", parser.NewTT2GoExp("foo.bar(param1, param2).something.else", false, noChildren)}, + {"complex method with literal params with chaining", "@foo.bar(\"hello\", 123).something.else\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123).something.else", false, noChildren)}, + + // Transclusion tests + { + "simple method with transclusion", + "@foobar() {\n\t
Hello
\n}", + parser.NewTT2GoExp( + "foobar()", + false, + simpleTransclusionChildren, + ), + }, + + { + "complex method with transclusion", + "@foo.bar() {\n\t
Hello
\n}", + parser.NewTT2GoExp( + "foo.bar()", + false, + simpleTransclusionChildren, + ), + }, + + { + "complex method with param with transclusion", + "@foo.bar(param1, param2) {\n\t
Hello
\n}", + parser.NewTT2GoExp( + "foo.bar(param1, param2)", + false, + simpleTransclusionChildren, + ), + }, + + { + "complex method with literal params with transclusion", + "@foo.bar(\"hello\", 123) {\n\t
Hello
\n}", + parser.NewTT2GoExp( + "foo.bar(\"hello\", 123)", + false, + simpleTransclusionChildren, + ), + }, +} + +func TestExpressionParsing(t *testing.T) { + runParserTest(expressionTests, t, func (p *parser.Parser2) *parser.TemplateTree2 { + return p.Expression() + },"") +} + diff --git a/internal/parser/parse_safe_expression_test.go b/internal/parser/parse_safe_expression_test.go new file mode 100644 index 0000000..8d92d8d --- /dev/null +++ b/internal/parser/parse_safe_expression_test.go @@ -0,0 +1,26 @@ +package parser_test + +import ( + "testing" + + "github.com/gamebox/gwirl/internal/parser" +) + +var safeExpressionTests = []ParsingTest{ + {"simple expression", "@(foobar)a", parser.NewTT2GoExpSafe("foobar", false)}, + {"complex expression", "@(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", false)}, + {"complex method with chaining", "@(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", false)}, + {"complex method with params with chaining", "@(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", false)}, + {"complex method with literal params with chaining", "@(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", false)}, + {"escaped simple expression", "@!(foobar)a", parser.NewTT2GoExpSafe("foobar", true)}, + {"escaped complex expression", "@!(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", true)}, + {"escaped complex method with chaining", "@!(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", true)}, + {"escaped complex method with params with chaining", "@!(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", true)}, + {"escaped complex method with literal params with chaining", "@!(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", true)}, +} + +func TestParseSafeExpression(t *testing.T) { + runParserTest(safeExpressionTests, t, func(p *parser.Parser2) *parser.TemplateTree2 { + return p.SafeExpression() + }, "") +} diff --git a/internal/parser/parserv2.go b/internal/parser/parserv2.go index 8c3842b..2b3821e 100644 --- a/internal/parser/parserv2.go +++ b/internal/parser/parserv2.go @@ -374,7 +374,28 @@ func expressionContainsKeyword(expressionCode string) (string, bool) { return "", false } -func (p *Parser2) expression() *TemplateTree2 { +func (p *Parser2) SafeExpression() *TemplateTree2 { + p.log("SafeExpression") + escape := p.checkStr("@!(") + if !escape && !p.checkStr("@(") { + return nil + } + var t *TemplateTree2 = nil + p.input.regress(1) + pos := p.input.offset() + 1 + code := p.parentheses(true) + if code == nil { + return t + } + content := strings.TrimPrefix(strings.TrimSuffix(*code, ")"), "(") + exp := NewTT2GoExpSafe(content, escape) + t = &exp + p.position(t, pos) + + return t +} + +func (p *Parser2) Expression() *TemplateTree2 { p.log("expression") if !p.checkStr("@") { return nil @@ -622,8 +643,14 @@ func (p *Parser2) Mixed() *TemplateTree2 { p.logf("mixedOpt1: got plain: %v", plain) return plain } + p.logf("mixedOpt1: trying SafeExpression") + safeExp := p.SafeExpression() + if safeExp != nil { + p.logf("SafeExpression was not null: %v\n", safeExp) + return safeExp + } p.logf("mixedOpt1: trying expression") - exp := p.expression() + exp := p.Expression() if exp != nil { p.logf("expression was not null: %v\n", exp) return exp diff --git a/internal/parser/testdata/testAll.html.gwirl b/internal/parser/testdata/testAll.html.gwirl index b413177..54a08b6 100644 --- a/internal/parser/testdata/testAll.html.gwirl +++ b/internal/parser/testdata/testAll.html.gwirl @@ -15,5 +15,6 @@

B.O.B.

} @else {

@name

+

This is a example of a need for a @(name)Safe expression

} diff --git a/internal/parser/testing_fixtures_test.go b/internal/parser/testing_fixtures_test.go new file mode 100644 index 0000000..3a9713b --- /dev/null +++ b/internal/parser/testing_fixtures_test.go @@ -0,0 +1,79 @@ +package parser_test + +import ( + "os" + "testing" + + "github.com/gamebox/gwirl/internal/parser" +) + +type ParsingTest struct { + name string + input string + expected parser.TemplateTree2 +} +var noChildren = [][]parser.TemplateTree2{} +var simpleTransclusionChildren = [][]parser.TemplateTree2{ + { + parser.NewTT2Plain("\n\t
Hello
\n"), + }, +} + +func compareTrees(a parser.TemplateTree2, b parser.TemplateTree2, t *testing.T) { + if a.Text != b.Text { + t.Fatalf("Expected \"%s\" but got \"%s\"", a.Text, b.Text) + } + if a.Type != b.Type { + t.Fatalf("Expected type %d but got %d", a.Type, b.Type) + } + if a.Metadata != b.Metadata { + t.Fatalf("Expected metadata %d but got %d", a.Metadata, b.Metadata) + } + if a.Children == nil && b.Children != nil { + t.Fatal("Expected the children to be nil") + } + if a.Children != nil && b.Children == nil { + t.Fatal("Expected the children to not be nil") + } + if len(a.Children) != len(b.Children) { + t.Fatalf("Expected %d children, got %d children", len(a.Children), len(b.Children)) + } + for i := range a.Children { + aChildTree, bChildtree := a.Children[i], b.Children[i] + if aChildTree == nil && bChildtree != nil { + t.Fatal("Expected the children to be nil") + } + if aChildTree != nil && bChildtree == nil { + t.Fatal("Expected the children to not be nil") + } + if len(aChildTree) != len(bChildtree) { + t.Fatalf("Expected child to have %d trees, got %d trees", len(a.Children), len(b.Children)) + } + for childIdx := range aChildTree { + compareTrees(aChildTree[childIdx], bChildtree[childIdx], t) + } + } +} + +func runParserTest(tests []ParsingTest, t *testing.T, parseFn func(*parser.Parser2) *parser.TemplateTree2, debug string) { + for i := range tests { + test := tests[i] + if debug != "" && debug != test.name { + continue + } + success := t.Run(test.name, func(t *testing.T) { + p := parser.NewParser2(test.input) + if debug != "" { + p.SetLogger(os.Stdout) + } + res := parseFn(&p) + if res == nil { + t.Fatal("Expected a result, got nil") + } + compareTrees(test.expected, *res, t) + }) + if !success { + t.FailNow() + } + } +}