Skip to content

Commit 177b79f

Browse files
committedMar 5, 2025
Support usecase where a nil variable value is used in a logical expression
1 parent 0cacc01 commit 177b79f

File tree

4 files changed

+44
-16
lines changed

4 files changed

+44
-16
lines changed
 

‎lib/liquid/expression.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Expression
2828
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
2929

3030
class << self
31-
def parse(markup, ss = StringScanner.new(""), cache = nil)
31+
def parse(markup, ss = StringScanner.new(""), cache = nil, logical_expression = false)
3232
return unless markup
3333

3434
markup = markup.strip # markup can be a frozen string
@@ -44,13 +44,13 @@ def parse(markup, ss = StringScanner.new(""), cache = nil)
4444
if cache
4545
return cache[markup] if cache.key?(markup)
4646

47-
cache[markup] = inner_parse(markup, ss, cache).freeze
47+
cache[markup] = inner_parse(markup, ss, cache, logical_expression).freeze
4848
else
49-
inner_parse(markup, ss, nil).freeze
49+
inner_parse(markup, ss, nil, logical_expression).freeze
5050
end
5151
end
5252

53-
def inner_parse(markup, ss, cache)
53+
def inner_parse(markup, ss, cache, logical_expression = false)
5454
return LogicalExpression.parse(markup, ss, cache) if LogicalExpression.logical?(markup)
5555
return ComparisonExpression.parse(markup, ss, cache) if ComparisonExpression.comparison?(markup)
5656

@@ -66,7 +66,7 @@ def inner_parse(markup, ss, cache)
6666
if (num = parse_number(markup, ss))
6767
num
6868
else
69-
VariableLookup.parse(markup, ss, cache)
69+
VariableLookup.parse(markup, ss, cache, logical_expression)
7070
end
7171
end
7272

‎lib/liquid/expression/logical_expression.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def parse(markup, ss, cache)
2626
elsif logical?(last_expr)
2727
LogicalExpression.parse(last_expr, ss, cache)
2828
else
29-
Condition.new(Expression.parse(last_expr, ss, cache), nil, nil)
29+
Condition.new(Expression.parse(last_expr, ss, cache, true), nil, nil)
3030
end
3131

3232
until expressions.empty?
@@ -40,7 +40,7 @@ def parse(markup, ss, cache)
4040
elsif logical?(expr)
4141
LogicalExpression.parse(expr, ss, cache)
4242
else
43-
Condition.new(Expression.parse(expr, ss, cache), nil, nil)
43+
Condition.new(Expression.parse(expr, ss, cache, true), nil, nil)
4444
end
4545

4646
if operator == 'and'

‎lib/liquid/variable_lookup.rb

+17-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ class VariableLookup
55
COMMAND_METHODS = ['size', 'first', 'last'].freeze
66

77
attr_reader :name, :lookups
8+
attr_accessor :logical_expression
89

9-
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
10-
new(markup, string_scanner, cache)
10+
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
11+
new(markup, string_scanner, cache, logical_expression)
1112
end
1213

13-
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
14+
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil, logical_expression = false)
15+
@logical_expression = logical_expression
1416
lookups = markup.scan(VariableParser)
1517

1618
name = lookups.shift
@@ -45,9 +47,17 @@ def lookup_command?(lookup_index)
4547
end
4648

4749
def evaluate(context)
50+
puts "variable_lookup #evaluate #{@name} #{logical_expression?}"
4851
name = context.evaluate(@name)
4952
object = context.find_variable(name)
5053

54+
# When evaluating a logical expression, this variable lookup is part of a chain of conditions
55+
# If the variable lookup returns nil, we must use the falsey value of the variable lookup
56+
# rather than nil which is reserved for the usecase of rendering nothing.
57+
if logical_expression? && object.nil?
58+
return false
59+
end
60+
5161
@lookups.each_index do |i|
5262
key = context.evaluate(@lookups[i])
5363

@@ -89,6 +99,10 @@ def ==(other)
8999
self.class == other.class && state == other.state
90100
end
91101

102+
def logical_expression?
103+
@logical_expression
104+
end
105+
92106
protected
93107

94108
def state

‎test/unit/boolean_unit_test.rb

+20-6
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ def test_nil_renders_as_empty_string
106106
end
107107

108108
def test_nil_comparison_with_blank
109-
assert_parity_todo!("nil_value == blank", "false")
110-
assert_parity_todo!("nil_value != blank", "true")
111-
assert_parity_todo!("undefined != blank", "true")
112-
assert_parity_todo!("undefined == blank", "false")
109+
assert_parity("nil_value == blank", "false")
110+
assert_parity("nil_value != blank", "true")
111+
assert_parity("undefined != blank", "true")
112+
assert_parity("undefined == blank", "false")
113113
end
114114

115115
def test_if_with_variables
@@ -121,7 +121,13 @@ def test_if_with_variables
121121
end
122122

123123
def test_nil_variable_in_and_expression
124-
assert_parity_todo!("x and true", "false", { "x" => nil })
124+
assert_parity("x and true", "false", { "x" => nil })
125+
assert_parity("true and x", "false", { "x" => nil })
126+
end
127+
128+
def test_boolean_variable_in_and_expression
129+
assert_parity("true and x", "false", { "x" => false })
130+
assert_parity("x and true", "false", { "x" => false })
125131
end
126132

127133
private
@@ -133,10 +139,18 @@ def assert_parity_todo!(liquid_expression, expected_result, args = {})
133139
end
134140

135141
def assert_parity(liquid_expression, expected_result, args = {})
136-
assert_parity_scenario(:condition, "{% if #{liquid_expression} %}true{% else %}false{% endif %}", expected_result, args)
142+
assert_condition(liquid_expression, expected_result, args)
143+
assert_expression(liquid_expression, expected_result, args)
144+
end
145+
146+
def assert_expression(liquid_expression, expected_result, args = {})
137147
assert_parity_scenario(:expression, "{{ #{liquid_expression} }}", expected_result, args)
138148
end
139149

150+
def assert_condition(liquid_condition, expected_result, args = {})
151+
assert_parity_scenario(:condition, "{% if #{liquid_condition} %}true{% else %}false{% endif %}", expected_result, args)
152+
end
153+
140154
def assert_parity_scenario(kind, template, exp_output, args = {})
141155
act_output = Liquid::Template.parse(template).render(args)
142156

0 commit comments

Comments
 (0)
Failed to load comments.