From 10c0de74b52b36ef46d9a04db7c51dc993612555 Mon Sep 17 00:00:00 2001 From: "Charles-P. Clermont" Date: Fri, 5 Dec 2025 09:35:08 -0500 Subject: [PATCH] Add support for parenthesized expressions --- lib/liquid/parser.rb | 15 ++++++++++- test/integration/assign_test.rb | 15 ++++++++++- test/integration/parsing_quirks_test.rb | 9 +++---- test/unit/parser_unit_test.rb | 34 ++++++++++++++++++++----- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/lib/liquid/parser.rb b/lib/liquid/parser.rb index 15a11bc3..e01117a2 100644 --- a/lib/liquid/parser.rb +++ b/lib/liquid/parser.rb @@ -100,7 +100,7 @@ module Liquid when :number number when :open_round - range_lookup + grouping_or_range_lookup else raise SyntaxError, "#{token} is not a valid expression" end @@ -131,6 +131,19 @@ module Liquid VariableLookup.new(name, lookups, command_flags) 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 consume(:open_round) first = expression diff --git a/test/integration/assign_test.rb b/test/integration/assign_test.rb index 69163ab9..b3a04252 100644 --- a/test/integration/assign_test.rb +++ b/test/integration/assign_test.rb @@ -35,13 +35,26 @@ class AssignTest < Minitest::Test ) 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 assert_match_syntax_error(/assign/, '{% assign foo not values %}.') end def test_assign_throws_on_unsupported_syntax assert_match_syntax_error( - "Expected dotdot but found pipe", + "Expected close_round but found pipe", "{% assign foo = ('X' | downcase) %}", ) end diff --git a/test/integration/parsing_quirks_test.rb b/test/integration/parsing_quirks_test.rb index 75954f93..7927444e 100644 --- a/test/integration/parsing_quirks_test.rb +++ b/test/integration/parsing_quirks_test.rb @@ -35,11 +35,10 @@ class ParsingQuirksTest < Minitest::Test assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") } end - def test_meaningless_parens_error - assert_raises(SyntaxError) do - markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" - Template.parse("{% if #{markup} %} YES {% endif %}") - end + def test_supported_parens + markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" + out = Template.parse("{% if #{markup} %} YES {% endif %}").render({ 'b' => 'bar', 'c' => 'baz' }) + assert_equal(' YES ', out) end def test_unexpected_characters_syntax_error diff --git a/test/unit/parser_unit_test.rb b/test/unit/parser_unit_test.rb index 003ea83b..a602c425 100644 --- a/test/unit/parser_unit_test.rb +++ b/test/unit/parser_unit_test.rb @@ -98,12 +98,12 @@ class ParserUnitTest < Minitest::Test expr = p.expression assert(expr.is_a?(BinaryExpression)) assert_equal('and', expr.operator) - assert_equal('==', expr.left_node.operator) - assert_equal('a', expr.left_node.left_node.name) - assert_equal('b', expr.left_node.right_node.name) - assert_equal('or', expr.right_node.operator) - assert_equal('c', expr.right_node.left_node.name) - assert_equal('d', expr.right_node.right_node.name) + assert_equal('==', expr.left_node.operator) + assert_equal('a', expr.left_node.left_node.name) + assert_equal('b', expr.left_node.right_node.name) + assert_equal('or', expr.right_node.operator) + assert_equal('c', expr.right_node.left_node.name) + assert_equal('d', expr.right_node.right_node.name) end def test_equality @@ -197,6 +197,28 @@ class ParserUnitTest < Minitest::Test assert_equal('(hi[5].wat..old)', p.expression_string) 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 p = new_parser("filter: hi.there[5], keyarg: 7") assert_equal('filter', p.consume(:id))