Skip to content

Commit

Permalink
feat(compiler): support for safe expressions (#27)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gamebox authored Jan 15, 2024
1 parent 5ed3282 commit 0306083
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 3 deletions.
9 changes: 8 additions & 1 deletion gwirl-lsp/template_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
15 changes: 15 additions & 0 deletions internal/parser/nodesv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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,
Expand Down
68 changes: 68 additions & 0 deletions internal/parser/parse_expressions_test.go
Original file line number Diff line number Diff line change
@@ -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<div>Hello</div>\n}",
parser.NewTT2GoExp(
"foobar()",
false,
simpleTransclusionChildren,
),
},

{
"complex method with transclusion",
"@foo.bar() {\n\t<div>Hello</div>\n}",
parser.NewTT2GoExp(
"foo.bar()",
false,
simpleTransclusionChildren,
),
},

{
"complex method with param with transclusion",
"@foo.bar(param1, param2) {\n\t<div>Hello</div>\n}",
parser.NewTT2GoExp(
"foo.bar(param1, param2)",
false,
simpleTransclusionChildren,
),
},

{
"complex method with literal params with transclusion",
"@foo.bar(\"hello\", 123) {\n\t<div>Hello</div>\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()
},"")
}

26 changes: 26 additions & 0 deletions internal/parser/parse_safe_expression_test.go
Original file line number Diff line number Diff line change
@@ -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()
}, "")
}
31 changes: 29 additions & 2 deletions internal/parser/parserv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/parser/testdata/testAll.html.gwirl
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
<h2>B.O.B.</h2>
} @else {
<h2>@name</h2>
<p>This is a example of a need for a @(name)Safe expression</p>
}
</div>
79 changes: 79 additions & 0 deletions internal/parser/testing_fixtures_test.go
Original file line number Diff line number Diff line change
@@ -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<div>Hello</div>\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()
}
}
}

0 comments on commit 0306083

Please sign in to comment.