Skip to content

Commit 0306083

Browse files
authored
feat(compiler): support for safe expressions (#27)
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
1 parent 5ed3282 commit 0306083

7 files changed

+226
-3
lines changed

gwirl-lsp/template_utils.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,20 @@ func absTokensForContent(tt []parser.TemplateTree2) []absToken {
251251
case parser.TT2GoExp:
252252
length := len(t.Text)
253253
var atToken absToken
254-
if t.Metadata.Has(parser.TTMDEscape) {
254+
if t.Metadata.Has(parser.TTMDSafe) && !t.Metadata.Has(parser.TTMDEscape) {
255+
atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator)
256+
} else if t.Metadata.Has(parser.TTMDEscape) && t.Metadata.Has(parser.TTMDSafe) {
257+
atToken = NewAbsToken(startLine, startCol-3, 3, lsp.SemanticTokenOperator)
258+
} else if t.Metadata.Has(parser.TTMDEscape) {
255259
atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator)
256260
} else {
257261
atToken = NewAbsToken(startLine, startCol-1, 1, lsp.SemanticTokenOperator)
258262
}
259263
token := NewAbsToken(startLine, startCol, length, lsp.SemanticTokenParameter)
260264
tokens = append(tokens, atToken, token)
265+
if t.Metadata.Has(parser.TTMDSafe) {
266+
tokens = append(tokens, NewAbsToken(startLine, startCol + uint32(length), 1, lsp.SemanticTokenOperator))
267+
}
261268
if t.Children == nil {
262269
continue
263270
}

internal/parser/nodesv2.go

+15
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ type MetadataFlag int
9999

100100
const (
101101
TTMDEscape MetadataFlag = 1 << iota
102+
TTMDSafe = 2
102103
)
103104

104105
func (f MetadataFlag) Has(flag MetadataFlag) bool { return f&flag != 0 }
@@ -188,6 +189,20 @@ func NewTT2GoExp(content string, escape bool, transclusions [][]TemplateTree2) T
188189
}
189190
}
190191

192+
func NewTT2GoExpSafe(content string, escape bool) TemplateTree2 {
193+
var metadata MetadataFlag
194+
metadata.Set(TTMDSafe)
195+
if escape {
196+
metadata.Set(TTMDEscape)
197+
}
198+
return TemplateTree2{
199+
Type: TT2GoExp,
200+
Text: content,
201+
Metadata: metadata,
202+
Children: [][]TemplateTree2{},
203+
}
204+
}
205+
191206
func NewTT2BlockComment(content string) TemplateTree2 {
192207
return TemplateTree2{
193208
Type: TT2BlockComment,
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package parser_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gamebox/gwirl/internal/parser"
7+
)
8+
9+
var expressionTests = []ParsingTest{
10+
{"simple expression", "@foobar\"", parser.NewTT2GoExp("foobar", false, noChildren)},
11+
{"simple method", "@foobar()\"", parser.NewTT2GoExp("foobar()", false, noChildren)},
12+
{"complex expression", "@foo.bar\"", parser.NewTT2GoExp("foo.bar", false, noChildren)},
13+
{"complex method", "@foo.bar()\"", parser.NewTT2GoExp("foo.bar()", false, noChildren)},
14+
{"complex method with params", "@foo.bar(param1, param2)\"", parser.NewTT2GoExp("foo.bar(param1, param2)", false, noChildren)},
15+
{"complex method with literal params", "@foo.bar(\"hello\", 123)\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123)", false, noChildren)},
16+
{"complex method with struct literal param", "@foo.bar(MyStruct{something, else}, 123)\"", parser.NewTT2GoExp("foo.bar(MyStruct{something, else}, 123)", false, noChildren)},
17+
{"complex method with chaining", "@foo.bar().something.else\"", parser.NewTT2GoExp("foo.bar().something.else", false, noChildren)},
18+
{"complex method with params with chaining", "@foo.bar(param1, param2).something.else\"", parser.NewTT2GoExp("foo.bar(param1, param2).something.else", false, noChildren)},
19+
{"complex method with literal params with chaining", "@foo.bar(\"hello\", 123).something.else\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123).something.else", false, noChildren)},
20+
21+
// Transclusion tests
22+
{
23+
"simple method with transclusion",
24+
"@foobar() {\n\t<div>Hello</div>\n}",
25+
parser.NewTT2GoExp(
26+
"foobar()",
27+
false,
28+
simpleTransclusionChildren,
29+
),
30+
},
31+
32+
{
33+
"complex method with transclusion",
34+
"@foo.bar() {\n\t<div>Hello</div>\n}",
35+
parser.NewTT2GoExp(
36+
"foo.bar()",
37+
false,
38+
simpleTransclusionChildren,
39+
),
40+
},
41+
42+
{
43+
"complex method with param with transclusion",
44+
"@foo.bar(param1, param2) {\n\t<div>Hello</div>\n}",
45+
parser.NewTT2GoExp(
46+
"foo.bar(param1, param2)",
47+
false,
48+
simpleTransclusionChildren,
49+
),
50+
},
51+
52+
{
53+
"complex method with literal params with transclusion",
54+
"@foo.bar(\"hello\", 123) {\n\t<div>Hello</div>\n}",
55+
parser.NewTT2GoExp(
56+
"foo.bar(\"hello\", 123)",
57+
false,
58+
simpleTransclusionChildren,
59+
),
60+
},
61+
}
62+
63+
func TestExpressionParsing(t *testing.T) {
64+
runParserTest(expressionTests, t, func (p *parser.Parser2) *parser.TemplateTree2 {
65+
return p.Expression()
66+
},"")
67+
}
68+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package parser_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gamebox/gwirl/internal/parser"
7+
)
8+
9+
var safeExpressionTests = []ParsingTest{
10+
{"simple expression", "@(foobar)a", parser.NewTT2GoExpSafe("foobar", false)},
11+
{"complex expression", "@(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", false)},
12+
{"complex method with chaining", "@(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", false)},
13+
{"complex method with params with chaining", "@(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", false)},
14+
{"complex method with literal params with chaining", "@(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", false)},
15+
{"escaped simple expression", "@!(foobar)a", parser.NewTT2GoExpSafe("foobar", true)},
16+
{"escaped complex expression", "@!(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", true)},
17+
{"escaped complex method with chaining", "@!(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", true)},
18+
{"escaped complex method with params with chaining", "@!(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", true)},
19+
{"escaped complex method with literal params with chaining", "@!(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", true)},
20+
}
21+
22+
func TestParseSafeExpression(t *testing.T) {
23+
runParserTest(safeExpressionTests, t, func(p *parser.Parser2) *parser.TemplateTree2 {
24+
return p.SafeExpression()
25+
}, "")
26+
}

internal/parser/parserv2.go

+29-2
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,28 @@ func expressionContainsKeyword(expressionCode string) (string, bool) {
374374
return "", false
375375
}
376376

377-
func (p *Parser2) expression() *TemplateTree2 {
377+
func (p *Parser2) SafeExpression() *TemplateTree2 {
378+
p.log("SafeExpression")
379+
escape := p.checkStr("@!(")
380+
if !escape && !p.checkStr("@(") {
381+
return nil
382+
}
383+
var t *TemplateTree2 = nil
384+
p.input.regress(1)
385+
pos := p.input.offset() + 1
386+
code := p.parentheses(true)
387+
if code == nil {
388+
return t
389+
}
390+
content := strings.TrimPrefix(strings.TrimSuffix(*code, ")"), "(")
391+
exp := NewTT2GoExpSafe(content, escape)
392+
t = &exp
393+
p.position(t, pos)
394+
395+
return t
396+
}
397+
398+
func (p *Parser2) Expression() *TemplateTree2 {
378399
p.log("expression")
379400
if !p.checkStr("@") {
380401
return nil
@@ -622,8 +643,14 @@ func (p *Parser2) Mixed() *TemplateTree2 {
622643
p.logf("mixedOpt1: got plain: %v", plain)
623644
return plain
624645
}
646+
p.logf("mixedOpt1: trying SafeExpression")
647+
safeExp := p.SafeExpression()
648+
if safeExp != nil {
649+
p.logf("SafeExpression was not null: %v\n", safeExp)
650+
return safeExp
651+
}
625652
p.logf("mixedOpt1: trying expression")
626-
exp := p.expression()
653+
exp := p.Expression()
627654
if exp != nil {
628655
p.logf("expression was not null: %v\n", exp)
629656
return exp

internal/parser/testdata/testAll.html.gwirl

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
<h2>B.O.B.</h2>
1616
} @else {
1717
<h2>@name</h2>
18+
<p>This is a example of a need for a @(name)Safe expression</p>
1819
}
1920
</div>
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package parser_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/gamebox/gwirl/internal/parser"
8+
)
9+
10+
type ParsingTest struct {
11+
name string
12+
input string
13+
expected parser.TemplateTree2
14+
}
15+
var noChildren = [][]parser.TemplateTree2{}
16+
var simpleTransclusionChildren = [][]parser.TemplateTree2{
17+
{
18+
parser.NewTT2Plain("\n\t<div>Hello</div>\n"),
19+
},
20+
}
21+
22+
func compareTrees(a parser.TemplateTree2, b parser.TemplateTree2, t *testing.T) {
23+
if a.Text != b.Text {
24+
t.Fatalf("Expected \"%s\" but got \"%s\"", a.Text, b.Text)
25+
}
26+
if a.Type != b.Type {
27+
t.Fatalf("Expected type %d but got %d", a.Type, b.Type)
28+
}
29+
if a.Metadata != b.Metadata {
30+
t.Fatalf("Expected metadata %d but got %d", a.Metadata, b.Metadata)
31+
}
32+
if a.Children == nil && b.Children != nil {
33+
t.Fatal("Expected the children to be nil")
34+
}
35+
if a.Children != nil && b.Children == nil {
36+
t.Fatal("Expected the children to not be nil")
37+
}
38+
if len(a.Children) != len(b.Children) {
39+
t.Fatalf("Expected %d children, got %d children", len(a.Children), len(b.Children))
40+
}
41+
for i := range a.Children {
42+
aChildTree, bChildtree := a.Children[i], b.Children[i]
43+
if aChildTree == nil && bChildtree != nil {
44+
t.Fatal("Expected the children to be nil")
45+
}
46+
if aChildTree != nil && bChildtree == nil {
47+
t.Fatal("Expected the children to not be nil")
48+
}
49+
if len(aChildTree) != len(bChildtree) {
50+
t.Fatalf("Expected child to have %d trees, got %d trees", len(a.Children), len(b.Children))
51+
}
52+
for childIdx := range aChildTree {
53+
compareTrees(aChildTree[childIdx], bChildtree[childIdx], t)
54+
}
55+
}
56+
}
57+
58+
func runParserTest(tests []ParsingTest, t *testing.T, parseFn func(*parser.Parser2) *parser.TemplateTree2, debug string) {
59+
for i := range tests {
60+
test := tests[i]
61+
if debug != "" && debug != test.name {
62+
continue
63+
}
64+
success := t.Run(test.name, func(t *testing.T) {
65+
p := parser.NewParser2(test.input)
66+
if debug != "" {
67+
p.SetLogger(os.Stdout)
68+
}
69+
res := parseFn(&p)
70+
if res == nil {
71+
t.Fatal("Expected a result, got nil")
72+
}
73+
compareTrees(test.expected, *res, t)
74+
})
75+
if !success {
76+
t.FailNow()
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)