Skip to content

Commit 3578406

Browse files
committed
Compile regexp during compilation
1 parent 0c841b9 commit 3578406

File tree

7 files changed

+126
-23
lines changed

7 files changed

+126
-23
lines changed

eval.go

+28-10
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,6 @@ func (n binaryNode) eval(env interface{}) (interface{}, error) {
158158
}
159159
return !ok, nil
160160

161-
case "matches":
162-
if isText(left) && isText(right) {
163-
matched, err := regexp.MatchString(toText(right), toText(left))
164-
if err != nil {
165-
return nil, err
166-
}
167-
return matched, nil
168-
}
169-
return nil, fmt.Errorf("operator matches not defined on (%T, %T)", left, right)
170-
171161
case "~":
172162
if isText(left) && isText(right) {
173163
return toText(left) + toText(right), nil
@@ -255,6 +245,34 @@ func makeRange(min, max int64) ([]float64, error) {
255245
return a, nil
256246
}
257247

248+
func (n matchesNode) eval(env interface{}) (interface{}, error) {
249+
left, err := Run(n.left, env)
250+
if err != nil {
251+
return nil, err
252+
}
253+
254+
if n.r != nil {
255+
if isText(left) {
256+
return n.r.MatchString(toText(left)), nil
257+
}
258+
}
259+
260+
right, err := Run(n.right, env)
261+
if err != nil {
262+
return nil, err
263+
}
264+
265+
if isText(left) && isText(right) {
266+
matched, err := regexp.MatchString(toText(right), toText(left))
267+
if err != nil {
268+
return nil, err
269+
}
270+
return matched, nil
271+
}
272+
273+
return nil, fmt.Errorf("operator matches doesn't defined on (%T, %T): %v", left, right, n)
274+
}
275+
258276
func (n propertyNode) eval(env interface{}) (interface{}, error) {
259277
v, err := Run(n.node, env)
260278
if err != nil {

eval_test.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ var evalTests = []evalTest{
265265
nil,
266266
true,
267267
},
268+
{
269+
`"seafood" matches "sea" ~ "food"`,
270+
nil,
271+
true,
272+
},
268273
{
269274
`not ("seafood" matches "[0-9]+") ? "a" : "b"`,
270275
nil,
@@ -317,7 +322,27 @@ var evalErrorTests = []evalErrorTest{
317322
{
318323
`"seafood" matches "a(b"`,
319324
nil,
320-
`error parsing regexp:`,
325+
"error parsing regexp: missing closing ): `a(b`",
326+
},
327+
{
328+
`"seafood" matches "a" ~ ")b"`,
329+
nil,
330+
"error parsing regexp: unexpected ): `a)b`",
331+
},
332+
{
333+
`1 matches "1" ~ "2"`,
334+
nil,
335+
"operator matches doesn't defined on (float64, string): (1 matches (\"1\" ~ \"2\"))",
336+
},
337+
{
338+
`1 matches "1"`,
339+
nil,
340+
"operator matches doesn't defined on (float64, string): (1 matches \"1\")",
341+
},
342+
{
343+
`"1" matches 1`,
344+
nil,
345+
"operator matches doesn't defined on (string, float64): (\"1\" matches 1)",
321346
},
322347
{
323348
`0 ? 1 : 2`,

node.go

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package expr
22

3+
import "regexp"
4+
35
// Node represents items of abstract syntax tree.
46
type Node interface{}
57

@@ -36,6 +38,12 @@ type binaryNode struct {
3638
right Node
3739
}
3840

41+
type matchesNode struct {
42+
r *regexp.Regexp
43+
left Node
44+
right Node
45+
}
46+
3947
type propertyNode struct {
4048
node Node
4149
property Node

parser.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package expr
22

33
import (
44
"fmt"
5+
"regexp"
56
"strconv"
67
"unicode/utf8"
78
)
@@ -179,7 +180,18 @@ func (p *parser) parseExpression(precedence int) (Node, error) {
179180
}
180181
}
181182

182-
node = binaryNode{operator: token.value, left: node, right: expr}
183+
if token.is(operator, "matches") {
184+
var r *regexp.Regexp
185+
if s, ok := expr.(textNode); ok {
186+
r, err = regexp.Compile(s.value)
187+
if err != nil {
188+
return nil, p.errorf("%v", err)
189+
}
190+
}
191+
node = matchesNode{r: r, left: node, right: expr}
192+
} else {
193+
node = binaryNode{operator: token.value, left: node, right: expr}
194+
}
183195
token = p.current
184196
continue
185197
}

parser_test.go

+39-11
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ var parseTests = []parseTest{
101101
"a ?: b",
102102
conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}},
103103
},
104-
{
105-
`"foo" matches "/foo/"`,
106-
binaryNode{"matches", textNode{"foo"}, textNode{"/foo/"}},
107-
},
108104
{
109105
"foo.bar().foo().baz[33]",
110106
propertyNode{propertyNode{methodNode{methodNode{nameNode{"foo"}, identifierNode{"bar"}, []Node{}}, identifierNode{"foo"}, []Node{}}, identifierNode{"baz"}}, numberNode{33}},
@@ -169,6 +165,10 @@ var parseErrorTests = []parseErrorTest{
169165
"{-}",
170166
"a map key must be a",
171167
},
168+
{
169+
"a matches 'a)(b'",
170+
"error parsing regexp: unexpected )",
171+
},
172172
}
173173

174174
func TestParse(t *testing.T) {
@@ -196,14 +196,42 @@ func TestParseError(t *testing.T) {
196196
}
197197
}
198198

199-
func TestParseErrorPosition(t *testing.T) {
200-
_, err := Parse("foo() + bar(**)")
201-
if err == nil {
202-
err = fmt.Errorf("<nil>")
199+
func TestParse_matches(t *testing.T) {
200+
node, err := Parse(`foo matches "foo"`)
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
205+
m, ok := node.(matchesNode)
206+
if !ok {
207+
t.Fatalf("expected to me matchesNode, got %T", node)
208+
}
209+
210+
if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, textNode{"foo"}) {
211+
t.Fatalf("left or right side of matches operator invalid: %#v", m)
212+
}
213+
214+
if m.r == nil {
215+
t.Fatal("regexp should be compiled")
216+
}
217+
}
218+
219+
func TestParse_matches_dynamic(t *testing.T) {
220+
node, err := Parse(`foo matches regex`)
221+
if err != nil {
222+
t.Fatal(err)
223+
}
224+
225+
m, ok := node.(matchesNode)
226+
if !ok {
227+
t.Fatalf("expected to me matchesNode, got %T", node)
228+
}
229+
230+
if !reflect.DeepEqual(m.left, nameNode{"foo"}) || !reflect.DeepEqual(m.right, nameNode{"regex"}) {
231+
t.Fatalf("left or right side of matches operator invalid: %#v", m)
203232
}
204233

205-
expected := "unexpected token operator(**)\nfoo() + bar(**)\n------------^"
206-
if err.Error() != expected {
207-
t.Errorf("\ngot\n\t%+v\nexpected\n\t%v", err.Error(), expected)
234+
if m.r != nil {
235+
t.Fatal("regexp should not be compiled")
208236
}
209237
}

print.go

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func (n binaryNode) String() string {
4444
return fmt.Sprintf("(%v %v %v)", n.left, n.operator, n.right)
4545
}
4646

47+
func (n matchesNode) String() string {
48+
return fmt.Sprintf("(%v matches %v)", n.left, n.right)
49+
}
50+
4751
func (n propertyNode) String() string {
4852
switch n.property.(type) {
4953
case identifierNode:

print_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ var printTests = []printTest{
4343
binaryNode{"and", binaryNode{"or", nameNode{"a"}, nameNode{"b"}}, nameNode{"c"}},
4444
"((a or b) and c)",
4545
},
46+
{
47+
matchesNode{left: nameNode{"foo"}, right: textNode{"foobar"}},
48+
"(foo matches \"foobar\")",
49+
},
50+
{
51+
conditionalNode{nameNode{"a"}, nameNode{"a"}, nameNode{"b"}},
52+
"a ? a : b",
53+
},
4654
}
4755

4856
func TestPrint(t *testing.T) {

0 commit comments

Comments
 (0)