Files
expr/parser/parser_test.go
T
9dd86858e1 feat: add node budget and memory limits (#762)
* feat: add node budget and memory limits

Implement compile-time and runtime safeguards against deeply nested expressions:

- Add MaxNodes limit to track and limit AST node count during parsing
- Move MemoryBudget from VM to Config for better configurability
- Add node creation tracking in parser to enforce node budget
- Refactor node creation to check limits before allocation
- Add RunWithConfig to VM for passing memory budget settings
- Set reasonable defaults in Config (10000 nodes, 1M memory)
- Add comprehensive tests for both node and memory limits

This prevents stack overflows from malicious or poorly written expressions
by failing fast with clear error messages during compilation or runtime.

Signed-off-by: Ville Vesilehto <[email protected]>

* Update parser.go

Signed-off-by: Anton Medvedev <[email protected]>

---------

Signed-off-by: Ville Vesilehto <[email protected]>
Signed-off-by: Anton Medvedev <[email protected]>
Co-authored-by: Anton Medvedev <[email protected]>
2025-03-07 14:09:41 +01:00

1004 lines
22 KiB
Go

package parser_test
import (
"fmt"
"strings"
"testing"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/testify/require"
. "github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/parser"
)
func TestParse(t *testing.T) {
tests := []struct {
input string
want Node
}{
{
"a",
&IdentifierNode{Value: "a"},
},
{
`"str"`,
&StringNode{Value: "str"},
},
{
"`hello\nworld`",
&StringNode{Value: `hello
world`},
},
{
"3",
&IntegerNode{Value: 3},
},
{
"0xFF",
&IntegerNode{Value: 255},
},
{
"0x6E",
&IntegerNode{Value: 110},
},
{
"0X63",
&IntegerNode{Value: 99},
},
{
"0o600",
&IntegerNode{Value: 384},
},
{
"0O45",
&IntegerNode{Value: 37},
},
{
"0b10",
&IntegerNode{Value: 2},
},
{
"0B101011",
&IntegerNode{Value: 43},
},
{
"10_000_000",
&IntegerNode{Value: 10_000_000},
},
{
"2.5",
&FloatNode{Value: 2.5},
},
{
"1e9",
&FloatNode{Value: 1e9},
},
{
"true",
&BoolNode{Value: true},
},
{
"false",
&BoolNode{Value: false},
},
{
"nil",
&NilNode{},
},
{
"-3",
&UnaryNode{Operator: "-",
Node: &IntegerNode{Value: 3}},
},
{
"-2^2",
&UnaryNode{
Operator: "-",
Node: &BinaryNode{
Operator: "^",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 2},
},
},
},
{
"1 - 2",
&BinaryNode{Operator: "-",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 2}},
},
{
"(1 - 2) * 3",
&BinaryNode{
Operator: "*",
Left: &BinaryNode{
Operator: "-",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 2},
},
Right: &IntegerNode{Value: 3},
},
},
{
"a or b or c",
&BinaryNode{Operator: "or",
Left: &BinaryNode{Operator: "or",
Left: &IdentifierNode{Value: "a"},
Right: &IdentifierNode{Value: "b"}},
Right: &IdentifierNode{Value: "c"}},
},
{
"a or b and c",
&BinaryNode{Operator: "or",
Left: &IdentifierNode{Value: "a"},
Right: &BinaryNode{Operator: "and",
Left: &IdentifierNode{Value: "b"},
Right: &IdentifierNode{Value: "c"}}},
},
{
"(a or b) and c",
&BinaryNode{Operator: "and",
Left: &BinaryNode{Operator: "or",
Left: &IdentifierNode{Value: "a"},
Right: &IdentifierNode{Value: "b"}},
Right: &IdentifierNode{Value: "c"}},
},
{
"2**4-1",
&BinaryNode{Operator: "-",
Left: &BinaryNode{Operator: "**",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 4}},
Right: &IntegerNode{Value: 1}},
},
{
"foo(bar())",
&CallNode{Callee: &IdentifierNode{Value: "foo"},
Arguments: []Node{&CallNode{Callee: &IdentifierNode{Value: "bar"},
Arguments: []Node{}}}},
},
{
`foo("arg1", 2, true)`,
&CallNode{Callee: &IdentifierNode{Value: "foo"},
Arguments: []Node{&StringNode{Value: "arg1"},
&IntegerNode{Value: 2},
&BoolNode{Value: true}}},
},
{
"foo.bar",
&MemberNode{Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"}},
},
{
"foo['all']",
&MemberNode{Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "all"}},
},
{
"foo.bar()",
&CallNode{Callee: &MemberNode{Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"}, Method: true},
Arguments: []Node{}},
},
{
`foo.bar("arg1", 2, true)`,
&CallNode{Callee: &MemberNode{Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"}, Method: true},
Arguments: []Node{&StringNode{Value: "arg1"},
&IntegerNode{Value: 2},
&BoolNode{Value: true}}},
},
{
"foo[3]",
&MemberNode{Node: &IdentifierNode{Value: "foo"},
Property: &IntegerNode{Value: 3}},
},
{
"true ? true : false",
&ConditionalNode{Cond: &BoolNode{Value: true},
Exp1: &BoolNode{Value: true},
Exp2: &BoolNode{}},
},
{
"a?[b]:c",
&ConditionalNode{Cond: &IdentifierNode{Value: "a"},
Exp1: &ArrayNode{Nodes: []Node{&IdentifierNode{Value: "b"}}},
Exp2: &IdentifierNode{Value: "c"}},
},
{
"a.b().c().d[33]",
&MemberNode{
Node: &MemberNode{
Node: &CallNode{
Callee: &MemberNode{
Node: &CallNode{
Callee: &MemberNode{
Node: &IdentifierNode{
Value: "a",
},
Property: &StringNode{
Value: "b",
},
Method: true,
},
Arguments: []Node{},
},
Property: &StringNode{
Value: "c",
},
Method: true,
},
Arguments: []Node{},
},
Property: &StringNode{
Value: "d",
},
},
Property: &IntegerNode{Value: 33}},
},
{
"'a' == 'b'",
&BinaryNode{Operator: "==",
Left: &StringNode{Value: "a"},
Right: &StringNode{Value: "b"}},
},
{
"+0 != -0",
&BinaryNode{Operator: "!=",
Left: &UnaryNode{Operator: "+",
Node: &IntegerNode{}},
Right: &UnaryNode{Operator: "-",
Node: &IntegerNode{}}},
},
{
"[a, b, c]",
&ArrayNode{Nodes: []Node{&IdentifierNode{Value: "a"},
&IdentifierNode{Value: "b"},
&IdentifierNode{Value: "c"}}},
},
{
"{foo:1, bar:2}",
&MapNode{Pairs: []Node{&PairNode{Key: &StringNode{Value: "foo"},
Value: &IntegerNode{Value: 1}},
&PairNode{Key: &StringNode{Value: "bar"},
Value: &IntegerNode{Value: 2}}}},
},
{
"{foo:1, bar:2, }",
&MapNode{Pairs: []Node{&PairNode{Key: &StringNode{Value: "foo"},
Value: &IntegerNode{Value: 1}},
&PairNode{Key: &StringNode{Value: "bar"},
Value: &IntegerNode{Value: 2}}}},
},
{
`{"a": 1, 'b': 2}`,
&MapNode{Pairs: []Node{&PairNode{Key: &StringNode{Value: "a"},
Value: &IntegerNode{Value: 1}},
&PairNode{Key: &StringNode{Value: "b"},
Value: &IntegerNode{Value: 2}}}},
},
{
"[1].foo",
&MemberNode{Node: &ArrayNode{Nodes: []Node{&IntegerNode{Value: 1}}},
Property: &StringNode{Value: "foo"}},
},
{
"{foo:1}.bar",
&MemberNode{Node: &MapNode{Pairs: []Node{&PairNode{Key: &StringNode{Value: "foo"},
Value: &IntegerNode{Value: 1}}}},
Property: &StringNode{Value: "bar"}},
},
{
"len(foo)",
&BuiltinNode{
Name: "len",
Arguments: []Node{
&IdentifierNode{Value: "foo"},
},
},
},
{
`foo matches "foo"`,
&BinaryNode{
Operator: "matches",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}},
},
{
`foo not matches "foo"`,
&UnaryNode{
Operator: "not",
Node: &BinaryNode{
Operator: "matches",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}}},
},
{
`foo matches regex`,
&BinaryNode{
Operator: "matches",
Left: &IdentifierNode{Value: "foo"},
Right: &IdentifierNode{Value: "regex"}},
},
{
`foo contains "foo"`,
&BinaryNode{
Operator: "contains",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}},
},
{
`foo not contains "foo"`,
&UnaryNode{
Operator: "not",
Node: &BinaryNode{Operator: "contains",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}}},
},
{
`foo startsWith "foo"`,
&BinaryNode{Operator: "startsWith",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}},
},
{
`foo endsWith "foo"`,
&BinaryNode{Operator: "endsWith",
Left: &IdentifierNode{Value: "foo"},
Right: &StringNode{Value: "foo"}},
},
{
"1..9",
&BinaryNode{Operator: "..",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 9}},
},
{
"0 in []",
&BinaryNode{Operator: "in",
Left: &IntegerNode{},
Right: &ArrayNode{Nodes: []Node{}}},
},
{
"not in_var",
&UnaryNode{Operator: "not",
Node: &IdentifierNode{Value: "in_var"}},
},
{
"-1 not in [1, 2, 3, 4]",
&UnaryNode{Operator: "not",
Node: &BinaryNode{Operator: "in",
Left: &UnaryNode{Operator: "-", Node: &IntegerNode{Value: 1}},
Right: &ArrayNode{Nodes: []Node{
&IntegerNode{Value: 1},
&IntegerNode{Value: 2},
&IntegerNode{Value: 3},
&IntegerNode{Value: 4},
}}}},
},
{
"1*8 not in [1, 2, 3, 4]",
&UnaryNode{Operator: "not",
Node: &BinaryNode{Operator: "in",
Left: &BinaryNode{Operator: "*",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 8},
},
Right: &ArrayNode{Nodes: []Node{
&IntegerNode{Value: 1},
&IntegerNode{Value: 2},
&IntegerNode{Value: 3},
&IntegerNode{Value: 4},
}}}},
},
{
"2==2 ? false : 3 not in [1, 2, 5]",
&ConditionalNode{
Cond: &BinaryNode{
Operator: "==",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 2},
},
Exp1: &BoolNode{Value: false},
Exp2: &UnaryNode{
Operator: "not",
Node: &BinaryNode{
Operator: "in",
Left: &IntegerNode{Value: 3},
Right: &ArrayNode{Nodes: []Node{
&IntegerNode{Value: 1},
&IntegerNode{Value: 2},
&IntegerNode{Value: 5},
}}}}},
},
{
"'foo' + 'bar' not matches 'foobar'",
&UnaryNode{Operator: "not",
Node: &BinaryNode{Operator: "matches",
Left: &BinaryNode{Operator: "+",
Left: &StringNode{Value: "foo"},
Right: &StringNode{Value: "bar"}},
Right: &StringNode{Value: "foobar"}}},
},
{
"all(Tickets, #)",
&BuiltinNode{
Name: "all",
Arguments: []Node{
&IdentifierNode{Value: "Tickets"},
&PredicateNode{
Node: &PointerNode{},
}}},
},
{
"all(Tickets, {.Price > 0})",
&BuiltinNode{
Name: "all",
Arguments: []Node{
&IdentifierNode{Value: "Tickets"},
&PredicateNode{
Node: &BinaryNode{
Operator: ">",
Left: &MemberNode{Node: &PointerNode{},
Property: &StringNode{Value: "Price"}},
Right: &IntegerNode{Value: 0}}}}},
},
{
"one(Tickets, {#.Price > 0})",
&BuiltinNode{
Name: "one",
Arguments: []Node{
&IdentifierNode{Value: "Tickets"},
&PredicateNode{
Node: &BinaryNode{
Operator: ">",
Left: &MemberNode{
Node: &PointerNode{},
Property: &StringNode{Value: "Price"},
},
Right: &IntegerNode{Value: 0}}}}},
},
{
"filter(Prices, {# > 100})",
&BuiltinNode{Name: "filter",
Arguments: []Node{&IdentifierNode{Value: "Prices"},
&PredicateNode{Node: &BinaryNode{Operator: ">",
Left: &PointerNode{},
Right: &IntegerNode{Value: 100}}}}},
},
{
"array[1:2]",
&SliceNode{Node: &IdentifierNode{Value: "array"},
From: &IntegerNode{Value: 1},
To: &IntegerNode{Value: 2}},
},
{
"array[:2]",
&SliceNode{Node: &IdentifierNode{Value: "array"},
To: &IntegerNode{Value: 2}},
},
{
"array[1:]",
&SliceNode{Node: &IdentifierNode{Value: "array"},
From: &IntegerNode{Value: 1}},
},
{
"array[:]",
&SliceNode{Node: &IdentifierNode{Value: "array"}},
},
{
"[]",
&ArrayNode{},
},
{
"foo ?? bar",
&BinaryNode{Operator: "??",
Left: &IdentifierNode{Value: "foo"},
Right: &IdentifierNode{Value: "bar"}},
},
{
"foo ?? bar ?? baz",
&BinaryNode{Operator: "??",
Left: &BinaryNode{Operator: "??",
Left: &IdentifierNode{Value: "foo"},
Right: &IdentifierNode{Value: "bar"}},
Right: &IdentifierNode{Value: "baz"}},
},
{
"foo ?? (bar || baz)",
&BinaryNode{Operator: "??",
Left: &IdentifierNode{Value: "foo"},
Right: &BinaryNode{Operator: "||",
Left: &IdentifierNode{Value: "bar"},
Right: &IdentifierNode{Value: "baz"}}},
},
{
"foo || bar ?? baz",
&BinaryNode{Operator: "||",
Left: &IdentifierNode{Value: "foo"},
Right: &BinaryNode{Operator: "??",
Left: &IdentifierNode{Value: "bar"},
Right: &IdentifierNode{Value: "baz"}}},
},
{
"foo ?? bar()",
&BinaryNode{Operator: "??",
Left: &IdentifierNode{Value: "foo"},
Right: &CallNode{Callee: &IdentifierNode{Value: "bar"}}},
},
{
"true | ok()",
&CallNode{
Callee: &IdentifierNode{Value: "ok"},
Arguments: []Node{
&BoolNode{Value: true}}}},
{
`let foo = a + b; foo + c`,
&VariableDeclaratorNode{
Name: "foo",
Value: &BinaryNode{Operator: "+",
Left: &IdentifierNode{Value: "a"},
Right: &IdentifierNode{Value: "b"}},
Expr: &BinaryNode{Operator: "+",
Left: &IdentifierNode{Value: "foo"},
Right: &IdentifierNode{Value: "c"}}},
},
{
`map([], #index)`,
&BuiltinNode{
Name: "map",
Arguments: []Node{
&ArrayNode{},
&PredicateNode{
Node: &PointerNode{Name: "index"},
},
},
},
},
{
`::split("a,b,c", ",")`,
&BuiltinNode{
Name: "split",
Arguments: []Node{
&StringNode{Value: "a,b,c"},
&StringNode{Value: ","},
},
},
},
{
`::split("a,b,c", ",")[0]`,
&MemberNode{
Node: &BuiltinNode{
Name: "split",
Arguments: []Node{
&StringNode{Value: "a,b,c"},
&StringNode{Value: ","},
},
},
Property: &IntegerNode{Value: 0},
},
},
{
`"hello"[1:3]`,
&SliceNode{
Node: &StringNode{Value: "hello"},
From: &IntegerNode{Value: 1},
To: &IntegerNode{Value: 3},
},
},
{
`1 < 2 > 3`,
&BinaryNode{
Operator: "&&",
Left: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 2},
},
Right: &BinaryNode{
Operator: ">",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 3},
},
},
},
{
`1 < 2 < 3 < 4`,
&BinaryNode{
Operator: "&&",
Left: &BinaryNode{
Operator: "&&",
Left: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 2},
},
Right: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 3},
},
},
Right: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 3},
Right: &IntegerNode{Value: 4},
},
},
},
{
`1 < 2 < 3 == true`,
&BinaryNode{
Operator: "==",
Left: &BinaryNode{
Operator: "&&",
Left: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 1},
Right: &IntegerNode{Value: 2},
},
Right: &BinaryNode{
Operator: "<",
Left: &IntegerNode{Value: 2},
Right: &IntegerNode{Value: 3},
},
},
Right: &BoolNode{Value: true},
},
},
{
"if a>b {true} else {x}",
&ConditionalNode{
Cond: &BinaryNode{
Operator: ">",
Left: &IdentifierNode{Value: "a"},
Right: &IdentifierNode{Value: "b"},
},
Exp1: &BoolNode{Value: true},
Exp2: &IdentifierNode{Value: "x"}},
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
actual, err := parser.Parse(test.input)
require.NoError(t, err)
assert.Equal(t, Dump(test.want), Dump(actual.Node))
})
}
}
const errorTests = `
foo.
unexpected end of expression (1:4)
| foo.
| ...^
a+
unexpected token EOF (1:2)
| a+
| .^
a ? (1+2) c
unexpected token Identifier("c") (1:11)
| a ? (1+2) c
| ..........^
[a b]
unexpected token Identifier("b") (1:4)
| [a b]
| ...^
foo.bar(a b)
unexpected token Identifier("b") (1:11)
| foo.bar(a b)
| ..........^
{-}
a map key must be a quoted string, a number, a identifier, or an expression enclosed in parentheses (unexpected token Operator("-")) (1:2)
| {-}
| .^
foo({.bar})
a map key must be a quoted string, a number, a identifier, or an expression enclosed in parentheses (unexpected token Operator(".")) (1:6)
| foo({.bar})
| .....^
.foo
cannot use pointer accessor outside predicate (1:1)
| .foo
| ^
[1, 2, 3,,]
unexpected token Operator(",") (1:10)
| [1, 2, 3,,]
| .........^
[,]
unexpected token Operator(",") (1:2)
| [,]
| .^
{,}
a map key must be a quoted string, a number, a identifier, or an expression enclosed in parentheses (unexpected token Operator(",")) (1:2)
| {,}
| .^
{foo:1, bar:2, ,}
unexpected token Operator(",") (1:16)
| {foo:1, bar:2, ,}
| ...............^
foo ?? bar || baz
Operator (||) and coalesce expressions (??) cannot be mixed. Wrap either by parentheses. (1:12)
| foo ?? bar || baz
| ...........^
0b15
bad number syntax: "0b15" (1:4)
| 0b15
| ...^
0X10G
bad number syntax: "0X10G" (1:5)
| 0X10G
| ....^
0o1E
invalid float literal: strconv.ParseFloat: parsing "0o1E": invalid syntax (1:4)
| 0o1E
| ...^
0b1E
invalid float literal: strconv.ParseFloat: parsing "0b1E": invalid syntax (1:4)
| 0b1E
| ...^
0b1E+6
bad number syntax: "0b1E+6" (1:6)
| 0b1E+6
| .....^
0b1E+1
invalid float literal: strconv.ParseFloat: parsing "0b1E+1": invalid syntax (1:6)
| 0b1E+1
| .....^
0o1E+1
invalid float literal: strconv.ParseFloat: parsing "0o1E+1": invalid syntax (1:6)
| 0o1E+1
| .....^
1E
invalid float literal: strconv.ParseFloat: parsing "1E": invalid syntax (1:2)
| 1E
| .^
1 not == [1, 2, 5]
unexpected token Operator("==") (1:7)
| 1 not == [1, 2, 5]
| ......^
`
func TestParse_error(t *testing.T) {
tests := strings.Split(strings.Trim(errorTests, "\n"), "\n\n")
for _, test := range tests {
input := strings.SplitN(test, "\n", 2)
if len(input) != 2 {
t.Errorf("syntax error in test: %q", test)
break
}
_, err := parser.Parse(input[0])
if err == nil {
err = fmt.Errorf("<nil>")
}
assert.Equal(t, input[1], err.Error(), input[0])
}
}
func TestParse_optional_chaining(t *testing.T) {
parseTests := []struct {
input string
expected Node
}{
{
"foo?.bar.baz",
&ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
Optional: true,
},
Property: &StringNode{Value: "baz"},
},
},
},
{
"foo.bar?.baz",
&ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
},
Property: &StringNode{Value: "baz"},
Optional: true,
},
},
},
{
"foo?.bar?.baz",
&ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
Optional: true,
},
Property: &StringNode{Value: "baz"},
Optional: true,
},
},
},
{
"!foo?.bar.baz",
&UnaryNode{
Operator: "!",
Node: &ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
Optional: true,
},
Property: &StringNode{Value: "baz"},
},
},
},
},
{
"foo.bar[a?.b]?.baz",
&ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
},
Property: &ChainNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "a"},
Property: &StringNode{Value: "b"},
Optional: true,
},
},
},
Property: &StringNode{Value: "baz"},
Optional: true,
},
},
},
{
"foo.bar?.[0]",
&ChainNode{
Node: &MemberNode{
Node: &MemberNode{
Node: &IdentifierNode{Value: "foo"},
Property: &StringNode{Value: "bar"},
},
Property: &IntegerNode{Value: 0},
Optional: true,
},
},
},
}
for _, test := range parseTests {
actual, err := parser.Parse(test.input)
if err != nil {
t.Errorf("%s:\n%v", test.input, err)
continue
}
assert.Equal(t, Dump(test.expected), Dump(actual.Node), test.input)
}
}
func TestParse_pipe_operator(t *testing.T) {
input := "arr | map(.foo) | len() | Foo()"
expect := &CallNode{
Callee: &IdentifierNode{Value: "Foo"},
Arguments: []Node{
&BuiltinNode{
Name: "len",
Arguments: []Node{
&BuiltinNode{
Name: "map",
Arguments: []Node{
&IdentifierNode{Value: "arr"},
&PredicateNode{
Node: &MemberNode{
Node: &PointerNode{},
Property: &StringNode{Value: "foo"},
}}}}}}}}
actual, err := parser.Parse(input)
require.NoError(t, err)
assert.Equal(t, Dump(expect), Dump(actual.Node))
}
func TestNodeBudget(t *testing.T) {
tests := []struct {
name string
expr string
maxNodes uint
shouldError bool
}{
{
name: "simple expression equal to limit",
expr: "a + b",
maxNodes: 3,
shouldError: false,
},
{
name: "medium expression under limit",
expr: "a + b * c / d",
maxNodes: 20,
shouldError: false,
},
{
name: "deeply nested expression over limit",
expr: "1 + (2 + (3 + (4 + (5 + (6 + (7 + 8))))))",
maxNodes: 10,
shouldError: true,
},
{
name: "array expression over limit",
expr: "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]",
maxNodes: 5,
shouldError: true,
},
{
name: "disabled node budget",
expr: "1 + (2 + (3 + (4 + (5 + (6 + (7 + 8))))))",
maxNodes: 0,
shouldError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := conf.CreateNew()
config.MaxNodes = tt.maxNodes
config.Disabled = make(map[string]bool, 0)
_, err := parser.ParseWithConfig(tt.expr, config)
hasError := err != nil && strings.Contains(err.Error(), "exceeds maximum allowed nodes")
if hasError != tt.shouldError {
t.Errorf("ParseWithConfig(%q) error = %v, shouldError %v", tt.expr, err, tt.shouldError)
}
// Verify error message format when expected
if tt.shouldError && err != nil {
expected := "compilation failed: expression exceeds maximum allowed nodes"
if !strings.Contains(err.Error(), expected) {
t.Errorf("Expected error message to contain %q, got %q", expected, err.Error())
}
}
})
}
}
func TestNodeBudgetDisabled(t *testing.T) {
config := conf.CreateNew()
config.MaxNodes = 0 // Disable node budget
expr := strings.Repeat("a + ", 1000) + "b"
_, err := parser.ParseWithConfig(expr, config)
if err != nil && strings.Contains(err.Error(), "exceeds maximum allowed nodes") {
t.Error("Node budget check should be disabled when MaxNodes is 0")
}
}