Skip to content

Commit 591d3a2

Browse files
committed
[WIP] Support for nested boolean expressions in parentheses
- Added unit tests for range syntax - Added logical expression unit tests - parser respects parentheses during expression traversal
1 parent f1b178d commit 591d3a2

File tree

3 files changed

+376
-8
lines changed

3 files changed

+376
-8
lines changed

lib/liquid/parser.rb

+22-8
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,7 @@ def expression
6060
when :string, :number
6161
consume
6262
when :open_round
63-
consume
64-
first = expression
65-
consume(:dotdot)
66-
last = expression
67-
consume(:close_round)
68-
"(#{first}..#{last})"
63+
consume_round_parentheses(token)
6964
else
7065
raise SyntaxError, "#{token} is not a valid expression"
7166
end
@@ -79,13 +74,32 @@ def expression
7974
operator = consume(:boolean_operator)
8075
left = expr
8176
right = expression
82-
83-
"#{left} #{operator} #{right}"
77+
if look(:close_round)
78+
"(#{left} #{operator} #{right})"
79+
else
80+
"#{left} #{operator} #{right}"
81+
end
8482
else
8583
expr
8684
end
8785
end
8886

87+
def consume_round_parentheses(token)
88+
consume
89+
first = expression
90+
dotdot_token = consume?(:dotdot)
91+
if dotdot_token
92+
last = expression
93+
consume(:close_round)
94+
"(#{first}..#{last})"
95+
elsif look(:close_round)
96+
consume(:close_round)
97+
first
98+
else
99+
raise SyntaxError, "#{token} is not a valid expression"
100+
end
101+
end
102+
89103
def argument
90104
str = +""
91105
# might be a keyword argument (identifier: expression)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
require 'test_boolean_helper'
5+
6+
class LogicalExpressionTest < Minitest::Test
7+
include Liquid
8+
9+
def setup
10+
@ss = StringScanner.new("")
11+
@cache = {}
12+
end
13+
14+
def test_logical_detection
15+
assert(Expression::LogicalExpression.logical?("foo and bar"))
16+
assert(Expression::LogicalExpression.logical?("foo or bar"))
17+
assert(Expression::LogicalExpression.logical?("true and false"))
18+
assert(Expression::LogicalExpression.logical?("1 or 0"))
19+
20+
refute(Expression::LogicalExpression.logical?("foo"))
21+
refute(Expression::LogicalExpression.logical?("1 == 1"))
22+
refute(Expression::LogicalExpression.logical?("a contains b"))
23+
refute(Expression::LogicalExpression.logical?("not foo"))
24+
end
25+
26+
def test_parenthesized_logical_detection
27+
assert(Expression::LogicalExpression.logical?("a and (b or c)"))
28+
assert(Expression::LogicalExpression.logical?("(a or b) and c"))
29+
end
30+
31+
def test_boolean_operator_detection
32+
assert(Expression::LogicalExpression.boolean_operator?("and"))
33+
assert(Expression::LogicalExpression.boolean_operator?("or"))
34+
35+
refute(Expression::LogicalExpression.boolean_operator?("not"))
36+
refute(Expression::LogicalExpression.boolean_operator?("=="))
37+
refute(Expression::LogicalExpression.boolean_operator?("contains"))
38+
refute(Expression::LogicalExpression.boolean_operator?("foo"))
39+
end
40+
41+
def test_basic_parsing
42+
result = Expression::LogicalExpression.parse("true and false", @ss, @cache)
43+
assert_instance_of(Condition, result)
44+
45+
result = Expression::LogicalExpression.parse("a or b", @ss, @cache)
46+
assert_instance_of(Condition, result)
47+
end
48+
49+
def test_parsing_with_different_expressions
50+
# Test with simple variable expressions
51+
result = Expression::LogicalExpression.parse("var1 and var2", @ss, @cache)
52+
assert_instance_of(Condition, result)
53+
54+
# Test with comparison expressions
55+
result = Expression::LogicalExpression.parse("a == 1 and b != 2", @ss, @cache)
56+
assert_instance_of(Condition, result)
57+
end
58+
59+
def test_parsing_complex_expressions
60+
# Test with nested logical expressions
61+
result = Expression::LogicalExpression.parse("a and b or c", @ss, @cache)
62+
assert_instance_of(Condition, result)
63+
64+
result = Expression::LogicalExpression.parse("a or b and c", @ss, @cache)
65+
assert_instance_of(Condition, result)
66+
end
67+
68+
def test_parsing_parenthesized_expressions
69+
result = Expression::LogicalExpression.parse("(a and b) or c", @ss, @cache)
70+
assert_instance_of(Condition, result)
71+
72+
result = Expression::LogicalExpression.parse("a and (b or c)", @ss, @cache)
73+
assert_instance_of(Condition, result)
74+
75+
# Test with complex expressions
76+
result = Expression::LogicalExpression.parse("(a or b) and (c or d)", @ss, @cache)
77+
assert_instance_of(Condition, result)
78+
end
79+
80+
def test_evaluation_of_parsed_expressions
81+
context = Liquid::Context.new(
82+
"a" => true,
83+
"b" => false,
84+
"c" => true,
85+
"d" => false,
86+
)
87+
88+
# Test simple logical expressions
89+
expr = Expression::LogicalExpression.parse("a and c", @ss, @cache)
90+
assert_equal(true, expr.evaluate(context))
91+
92+
expr = Expression::LogicalExpression.parse("a and b", @ss, @cache)
93+
assert_equal(false, expr.evaluate(context))
94+
95+
expr = Expression::LogicalExpression.parse("b or c", @ss, @cache)
96+
assert_equal(true, expr.evaluate(context))
97+
98+
expr = Expression::LogicalExpression.parse("b or d", @ss, @cache)
99+
assert_equal(false, expr.evaluate(context))
100+
end
101+
102+
def test_evaluation_of_complex_expressions
103+
context = Liquid::Context.new(
104+
"a" => true,
105+
"b" => false,
106+
"c" => true,
107+
"d" => false,
108+
)
109+
110+
# Test complex logical expressions
111+
expr = Expression::LogicalExpression.parse("a and b or c", @ss, @cache)
112+
assert_equal(true, expr.evaluate(context))
113+
end
114+
115+
def test_evaluation_of_parenthesized_expressions
116+
context = Liquid::Context.new(
117+
"a" => true,
118+
"b" => false,
119+
"c" => true,
120+
"d" => false,
121+
)
122+
123+
expr = Expression::LogicalExpression.parse("a and (b or d)", @ss, @cache)
124+
assert_equal(false, expr.evaluate(context))
125+
126+
expr = Expression::LogicalExpression.parse("(a or b) and (c or d)", @ss, @cache)
127+
assert_equal(true, expr.evaluate(context))
128+
129+
expr = Expression::LogicalExpression.parse("(a or b) and (b or d)", @ss, @cache)
130+
assert_equal(false, expr.evaluate(context))
131+
end
132+
133+
def test_precedence_rules
134+
context = Liquid::Context.new(
135+
"a" => true,
136+
"b" => false,
137+
"c" => true,
138+
)
139+
140+
# Test precedence rules (AND has higher precedence than OR)
141+
# This should be interpreted as: a and (b or c)
142+
expr1 = Expression::LogicalExpression.parse("a and b or c", @ss, @cache)
143+
assert_equal(true, expr1.evaluate(context))
144+
145+
# Change context to make the expressions evaluate differently
146+
context = Liquid::Context.new(
147+
"a" => false,
148+
"b" => false,
149+
"c" => true,
150+
)
151+
152+
# With these values, "a and (b or c)" would be false
153+
expr1 = Expression::LogicalExpression.parse("a and b or c", @ss, @cache)
154+
assert_equal(false, expr1.evaluate(context))
155+
end
156+
157+
def test_precedence_with_parentheses
158+
context = Liquid::Context.new(
159+
"a" => true,
160+
"b" => false,
161+
"c" => true,
162+
)
163+
164+
# This should be interpreted as: (a and b) or c
165+
expr2 = Expression::LogicalExpression.parse("(a and b) or c", @ss, @cache)
166+
assert_equal(true, expr2.evaluate(context))
167+
168+
# Change context to make the expressions evaluate differently
169+
context = Liquid::Context.new(
170+
"a" => false,
171+
"b" => false,
172+
"c" => true,
173+
)
174+
175+
# But "(a and b) or c" would be true
176+
expr2 = Expression::LogicalExpression.parse("(a and b) or c", @ss, @cache)
177+
assert_equal(true, expr2.evaluate(context))
178+
end
179+
180+
def test_integration_with_if_tag
181+
# Test that our expressions work properly in actual templates
182+
assert_template_result("true", "{% if true and true %}true{% else %}false{% endif %}")
183+
assert_template_result("false", "{% if true and false %}true{% else %}false{% endif %}")
184+
assert_template_result("true", "{% if false or true %}true{% else %}false{% endif %}")
185+
assert_template_result("false", "{% if false or false %}true{% else %}false{% endif %}")
186+
end
187+
188+
def test_integration_with_parenthesized_if_tag
189+
# Test with parenthesized expressions
190+
assert_template_result("true", "{% if (true and false) or true %}true{% else %}false{% endif %}")
191+
assert_template_result("false", "{% if true and (false or false) %}true{% else %}false{% endif %}")
192+
assert_template_result("true", "{% if true and (false or true) %}true{% else %}false{% endif %}")
193+
end
194+
195+
def test_integration_with_variables
196+
# Test with variables
197+
template = "{% if a and b %}true{% else %}false{% endif %}"
198+
assert_template_result("true", template, { "a" => true, "b" => true })
199+
assert_template_result("false", template, { "a" => true, "b" => false })
200+
201+
template = "{% if a or b %}true{% else %}false{% endif %}"
202+
assert_template_result("true", template, { "a" => true, "b" => false })
203+
assert_template_result("false", template, { "a" => false, "b" => false })
204+
end
205+
206+
def test_integration_with_parenthesized_variables
207+
# Test with parenthesized expressions
208+
template = "{% if (a and b) or c %}true{% else %}false{% endif %}"
209+
assert_template_result("true", template, { "a" => true, "b" => true, "c" => false })
210+
assert_template_result("true", template, { "a" => false, "b" => false, "c" => true })
211+
assert_template_result("false", template, { "a" => false, "b" => false, "c" => false })
212+
213+
template = "{% if a and (b or c) %}true{% else %}false{% endif %}"
214+
assert_template_result("true", template, { "a" => true, "b" => true, "c" => false })
215+
assert_template_result("true", template, { "a" => true, "b" => false, "c" => true })
216+
assert_template_result("false", template, { "a" => true, "b" => false, "c" => false })
217+
assert_template_result("false", template, { "a" => false, "b" => true, "c" => true })
218+
end
219+
end

0 commit comments

Comments
 (0)