Add support for parenthesized expressions

This commit is contained in:
Charles-P. Clermont
2026-01-27 10:07:35 -05:00
parent f9b5313020
commit 10c0de74b5
4 changed files with 60 additions and 13 deletions
+14 -1
View File
@@ -100,7 +100,7 @@ module Liquid
when :number when :number
number number
when :open_round when :open_round
range_lookup grouping_or_range_lookup
else else
raise SyntaxError, "#{token} is not a valid expression" raise SyntaxError, "#{token} is not a valid expression"
end end
@@ -131,6 +131,19 @@ module Liquid
VariableLookup.new(name, lookups, command_flags) VariableLookup.new(name, lookups, command_flags)
end end
# Parenthesized expressions are recursive
def grouping_or_range_lookup
consume(:open_round)
expr = expression
if consume?(:dotdot)
RangeLookup.create(expr, expression)
else
expr
end
ensure
consume(:close_round)
end
def range_lookup def range_lookup
consume(:open_round) consume(:open_round)
first = expression first = expression
+14 -1
View File
@@ -35,13 +35,26 @@ class AssignTest < Minitest::Test
) )
end end
def test_assign_boolean_expression_assignment
assert_template_result(
'it rendered',
<<~LIQUID,
{%- assign should_render = a == 0 or (b == 1 and c == 2) -%}
{%- if should_render -%}
it rendered
{%- endif -%}
LIQUID
{ 'b' => 1, 'c' => 2 },
)
end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, '{% assign foo not values %}.') assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
end end
def test_assign_throws_on_unsupported_syntax def test_assign_throws_on_unsupported_syntax
assert_match_syntax_error( assert_match_syntax_error(
"Expected dotdot but found pipe", "Expected close_round but found pipe",
"{% assign foo = ('X' | downcase) %}", "{% assign foo = ('X' | downcase) %}",
) )
end end
+4 -5
View File
@@ -35,11 +35,10 @@ class ParsingQuirksTest < Minitest::Test
assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") } assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") }
end end
def test_meaningless_parens_error def test_supported_parens
assert_raises(SyntaxError) do markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" out = Template.parse("{% if #{markup} %} YES {% endif %}").render({ 'b' => 'bar', 'c' => 'baz' })
Template.parse("{% if #{markup} %} YES {% endif %}") assert_equal(' YES ', out)
end
end end
def test_unexpected_characters_syntax_error def test_unexpected_characters_syntax_error
+28 -6
View File
@@ -98,12 +98,12 @@ class ParserUnitTest < Minitest::Test
expr = p.expression expr = p.expression
assert(expr.is_a?(BinaryExpression)) assert(expr.is_a?(BinaryExpression))
assert_equal('and', expr.operator) assert_equal('and', expr.operator)
assert_equal('==', expr.left_node.operator) assert_equal('==', expr.left_node.operator)
assert_equal('a', expr.left_node.left_node.name) assert_equal('a', expr.left_node.left_node.name)
assert_equal('b', expr.left_node.right_node.name) assert_equal('b', expr.left_node.right_node.name)
assert_equal('or', expr.right_node.operator) assert_equal('or', expr.right_node.operator)
assert_equal('c', expr.right_node.left_node.name) assert_equal('c', expr.right_node.left_node.name)
assert_equal('d', expr.right_node.right_node.name) assert_equal('d', expr.right_node.right_node.name)
end end
def test_equality def test_equality
@@ -197,6 +197,28 @@ class ParserUnitTest < Minitest::Test
assert_equal('(hi[5].wat..old)', p.expression_string) assert_equal('(hi[5].wat..old)', p.expression_string)
end end
def test_groupings_aka_parenthesized_expressions
# without the parens, this would be evaled as a and (b or c)
p = new_parser("(a and b) or c")
expr = p.expression
assert_equal('or', expr.operator)
assert_equal('and', expr.left_node.operator)
assert_equal('a', expr.left_node.left_node.name)
assert_equal('b', expr.left_node.right_node.name)
assert_equal('c', expr.right_node.name)
end
def test_groupings_can_be_used_to_hijack_operation_priority
# without parens would be parsed as `a and (b == c)`
p = new_parser("(a and b) == c")
expr = p.expression
assert_equal('==', expr.operator)
assert_equal('and', expr.left_node.operator)
assert_equal('a', expr.left_node.left_node.name)
assert_equal('b', expr.left_node.right_node.name)
assert_equal('c', expr.right_node.name)
end
def test_argument_string def test_argument_string
p = new_parser("filter: hi.there[5], keyarg: 7") p = new_parser("filter: hi.there[5], keyarg: 7")
assert_equal('filter', p.consume(:id)) assert_equal('filter', p.consume(:id))