Tobi Lutke and Claude Opus 4.5
ccd05e869c
Make blank/empty comparisons invariant to ActiveSupport
...
- Implement liquid_blank? and liquid_empty? methods in Condition
to emulate ActiveSupport's behavior when it's not loaded
- This ensures templates like `{% if x == blank %}` work identically
whether ActiveSupport is loaded or not
- Update liquid-spec adapters for new API (ctx parameter)
- Add rake spec task for running liquid-spec matrix
🤖 Generated with [Claude Code](https://claude.com/claude-code )
Co-Authored-By: Claude Opus 4.5 <[email protected] >
2026-01-05 14:54:43 -10:00
Tobi Lutke
ef13b2dfd5
Fix empty? semantics and string first/last for empty strings
...
- nil is NOT empty (but IS blank) - matches Shopify production
- String first/last returns '' for empty strings, not nil - matches ActiveSupport
- Add test for nil not being empty
2026-01-01 22:06:22 -05:00
Tobi Lutke
af58800c16
Update rubocop-shopify to 2.18.0 and fix new offenses
2026-01-01 20:22:19 -05:00
Tobi Lutke
0e3548d39e
Remove redundant else-clause
2026-01-01 20:16:24 -05:00
Tobi Lutke
33bac87a5c
Address liquid-spec issues without ActiveSupport loaded
...
Implement ActiveSupport-compatible behaviors internally so Liquid works
correctly without ActiveSupport being loaded:
1. String first/last via property access (name.first, name.last)
- VariableLookup now handles string[0] and string[-1] for first/last
2. String first/last via filters (name | first, name | last)
- StandardFilters#first and #last now handle strings
3. blank?/empty? comparisons for types without these methods
- Condition now implements liquid_blank? and liquid_empty? internally
- blank? matches ActiveSupport: nil, false, empty/whitespace strings,
empty arrays/hashes are all blank
- empty? checks length == 0 only (whitespace is NOT empty)
This fixes spec failures for templates like:
- {{ name.first }} / {{ name | first }} on strings
- {% if x == blank %} for whitespace strings, empty hashes/arrays
- {% case ' ' %}{% when blank %} matching whitespace
2026-01-01 20:14:21 -05:00
Charles-P. Clermont and Guilherme Carreiro
94bbf6ca32
Make it possible to safe_parse subsets of expressions
...
e.g. sometimes you want to only accept strings | lookups.
{% render snippetName %} for example. snippetName is a string right now.
We don't want safe_parse_expression because this would allow snippetName
to be a number, a boolean, etc. But we still want to strict parse this.
So what we'll do is use parse_expression(string, safe: true), this is
an optional opt-in to say "I know what I'm doing". Usually that's
because you're using the output of Parser#something as the input of
parse_expression.
It is true that Parser#expression is subset of Expression.parse, it is
not true of the opposite (e.g. Expression.parse doesn't care about .5
and happily parses that as a global lookup of the variable named "5",
Parser#expression throws for that.)
diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb
index e5c321dc..9ab350f0 100644
--- a/lib/liquid/condition.rb
+++ b/lib/liquid/condition.rb
@@ -48,8 +48,8 @@ module Liquid
@@operators
end
- def self.parse_expression(parse_context, markup)
- @@method_literals[markup] || parse_context.parse_expression(markup)
+ def self.parse_expression(parse_context, markup, safe: false)
+ @@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
end
attr_reader :attachment, :child_condition
diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb
index 1c59fe4a..82cf5768 100644
--- a/lib/liquid/parse_context.rb
+++ b/lib/liquid/parse_context.rb
@@ -51,13 +51,13 @@ module Liquid
end
def safe_parse_expression(parser)
- Expression.safe_parse(parser)
+ Expression.safe_parse(parser, @string_scanner, @expression_cache)
end
- def parse_expression(markup)
+ def parse_expression(markup, safe: false)
# todo(guilherme): remove this once rigid mode is fully using safe_parse_expression
- # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if @error_mode == :rigid
- puts("🚨 parse_expression used in rigid mode") if @error_mode == :rigid
+ # raise Liquid::InternalError, "parse_expression is not supported in rigid mode" if !safe && @error_mode == :rigid
+ puts("🚨 parse_expression used in rigid mode") if !safe && @error_mode == :rigid
Expression.parse(markup, @string_scanner, @expression_cache)
end
diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb
index 656d2e47..374ee511 100644
--- a/lib/liquid/tag.rb
+++ b/lib/liquid/tag.rb
@@ -72,8 +72,8 @@ module Liquid
parse_context.safe_parse_expression(parser)
end
- def parse_expression(markup)
- parse_context.parse_expression(markup)
+ def parse_expression(markup, safe: false)
+ parse_context.parse_expression(markup, safe: safe)
end
end
end
diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb
index c2be5db1..3182983b 100644
--- a/lib/liquid/tags/for.rb
+++ b/lib/liquid/tags/for.rb
@@ -93,7 +93,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
- @collection_name = parse_expression(collection_name)
+ @collection_name = parse_expression(collection_name, safe: true)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb
index 342374f1..e25d6250 100644
--- a/lib/liquid/tags/if.rb
+++ b/lib/liquid/tags/if.rb
@@ -81,8 +81,8 @@ module Liquid
block.attach(new_body)
end
- def parse_expression(markup)
- Condition.parse_expression(parse_context, markup)
+ def parse_expression(markup, safe: false)
+ Condition.parse_expression(parse_context, markup, safe: safe)
end
def lax_parse(markup)
@@ -124,9 +124,9 @@ module Liquid
end
def parse_comparison(p)
- a = parse_expression(p.expression)
+ a = parse_expression(p.expression, safe: true)
if (op = p.consume?(:comparison))
- b = parse_expression(p.expression)
+ b = parse_expression(p.expression, safe: true)
Condition.new(a, op, b)
else
Condition.new(a)
diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb
index 6cdbfd6f..b72a235b 100644
--- a/lib/liquid/tags/include.rb
+++ b/lib/liquid/tags/include.rb
@@ -87,10 +87,11 @@ module Liquid
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
- template_name = p.expression
+ @template_name_expr = safe_parse_expression(p)
with_or_for = p.id?("for") || p.id?("with") || nil
+ @variable_name_expr = nil
if with_or_for
- variable_name = p.expression
+ @variable_name_expr = parse_expression(p.consume(:id), safe: true)
end
alias_name = nil
@@ -98,8 +99,6 @@ module Liquid
alias_name = p.consume(:id)
end
- @template_name_expr = parse_expression(template_name)
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@alias_name = alias_name
# optional comma
@@ -109,7 +108,7 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
- @attributes[key] = parse_expression(p.expression)
+ @attributes[key] = safe_parse_expression(p)
p.consume?(:comma) # optional comma
end
end
diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb
index 89c11063..4f716b24 100644
--- a/lib/liquid/tags/render.rb
+++ b/lib/liquid/tags/render.rb
@@ -88,10 +88,11 @@ module Liquid
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
- template_name = rigid_template_name(p)
+ @template_name_expr = parse_expression(rigid_template_name(p), safe: true)
+ @variable_name_expr = nil
with_or_for = p.id?("for") || p.id?("with") || nil
if with_or_for
- variable_name = p.expression
+ @variable_name_expr = safe_parse_expression(p)
end
alias_name = nil
@@ -99,8 +100,6 @@ module Liquid
alias_name = p.consume(:id)
end
- @template_name_expr = parse_expression(template_name)
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@alias_name = alias_name
@is_for_loop = (with_or_for == FOR)
@@ -111,7 +110,7 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
- @attributes[key] = parse_expression(p.expression)
+ @attributes[key] = safe_parse_expression(p)
p.consume?(:comma) # optional comma
end
end
diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb
index 20957065..a3623bc5 100644
--- a/lib/liquid/variable.rb
+++ b/lib/liquid/variable.rb
@@ -65,11 +65,11 @@ module Liquid
return if p.look(:end_of_string)
- @name = parse_context.parse_expression(p.expression)
+ @name = parse_context.safe_parse_expression(p)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
- @filters << parse_filter_expressions(filtername, filterargs)
+ @filters << parse_filter_expressions(filtername, filterargs, safe: true)
end
p.consume(:end_of_string)
end
@@ -122,15 +122,15 @@ module Liquid
private
- def parse_filter_expressions(filter_name, unparsed_args)
+ def parse_filter_expressions(filter_name, unparsed_args, safe: false)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
- if (matches = a.match(JustTagAttributes))
+ if (matches = a.match(JustTagAttributes)) # we'll need to fix this
keyword_args ||= {}
- keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
+ keyword_args[matches[1]] = parse_context.parse_expression(matches[2], safe: false)
else
- filter_args << parse_context.parse_expression(a)
+ filter_args << parse_context.parse_expression(a, safe: safe)
end
end
result = [filter_name, filter_args]
2025-10-27 16:33:31 +01:00
Ian Ker-Seymer
1b2b62964e
Allow for binary comparison of incompatible strings
2024-01-30 19:03:05 -05:00
Marco Rudilosso
21d6197533
Call to_liquid_value when short circuiting conditions
2023-07-19 15:22:12 +00:00
Guilherme Carreiro and GitHub
9ab688eada
Update rubocop-shopify (2.7.0 -> 2.12.0) ( #1687 )
2023-02-15 08:40:59 +01:00
Thierry Joyal
1d2bee1f60
Condition#evaluate to receive mandatory context argument
2022-03-02 14:35:31 -05:00
Charles-P. Clermont
e86fe27259
Fix lint
2021-09-09 11:22:39 -04:00
Michael Go
f686c5dec7
use Utils.to_liquid_value on conditionals
2021-06-14 18:19:37 -03:00
Dylan Thacker-Smith and GitHub
10ea6144e0
Add Liquid::ParseContext#parse_expression for liquid-c node disabling ( #1333 )
...
We would like to be able to disable liquid-c VM rendering at runtime,
but right now expression parsing is done using Expression.parse, which
isn't aware of the parse context. That prevents us from conditionally
compiling to VM code based on a parse option.
2020-10-27 11:00:04 -04:00
Dylan Thacker-Smith and GitHub
a03f02789b
Only use MethodLiteral in condition expressions ( #1300 )
2020-09-25 11:10:33 -04:00
Alessandro Diogo Brückheimer and Mike Angell
e83b1e4159
Add ForceEqualSignAlignment to .rubocop.yml ( #1190 )
...
* Add ForceEqualSignAlignment to .rubocop.yml
* Revert ForceEqualSignAlignment cop
* Update method alignment
* Undo addition of whitespace to improve readability
* Fix missing alignment
2019-10-21 21:18:48 +10:00
0d26f05bb8
Enabled frozen string literals ( #1154 )
...
* Enabled frozen string literals
* Update rubocop config
* Prefer string interpolation in simple cases
Co-Authored-By: Dylan Thacker-Smith <[email protected] >
2019-09-18 13:19:45 +10:00
Mike Angell
799da202df
Apply simple rubocop fixes
2019-08-31 21:58:33 +10:00
Stephen Paul Weber
7d13d88258
s/Traversal/ParseTreeVisitor
2018-10-18 09:38:33 -04:00
Stephen Paul Weber
c11fc656cf
Colocate Traversal classes with classes they traverse
...
This puts all knowledge of the traversal in the same file, and removes
the need for a CASES registry.
2018-10-18 09:37:48 -04:00
Stephen Paul Weber
d789ec4175
Liquid::Traversal
...
This enables traversal over whole document tree.
2018-10-15 10:11:58 -04:00
Dylan Thacker-Smith and GitHub
8928454e29
Use a loop to evaluate binary comparisions to avoid recursion ( #891 )
...
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:24 -04:00
Dylan Thacker-Smith and GitHub
2bb3552033
Fix internal liquid error when comparing hash with incompatible type ( #849 )
2017-01-16 13:13:17 -05:00
Florian Weingarten
4c22cef341
blank and empty as variable names
2015-06-04 12:30:50 -04:00
Dylan Thacker-Smith
8a2947865b
Avoid an exception from checking if a string contains a non-string.
2015-06-03 02:21:51 -04:00
Florian Weingarten
3372ca8136
Rubocop
2015-05-14 14:37:18 +00:00
Dylan Thacker-Smith
a1a128db19
Refactor Condition so that it takes a parsed expression.
2014-10-18 15:03:40 -04:00
Dylan Thacker-Smith
37260f17ff
Use Expression.parse and Context#evaluate in the Condition class.
2014-10-18 14:27:58 -04:00
Florian Weingarten
7196a2d58e
Avoid parallel assignments
2014-10-18 13:58:32 +00:00
Bogdan Gusiev
4e9d414fde
Fixed condition constains operator with wrong data type
...
"contains" operator on wrong data type should not cause NoMethodError.
2014-08-18 17:32:29 +03:00
Bogdan Gusiev
fa14fd02e7
Raise Liquid::ArugmentError when condition has wrong usage
...
Condition now raises ::ArgumentError when built wrongly.
This patch make it raise Liquid::ArgumentError instead
to indicate a liquid markup error instead of ruby error.
2014-04-21 16:42:37 +03:00
Florian Weingarten
0a7de51e2b
Revert some freezes on non-literals
2014-01-27 14:56:07 -05:00
Florian Weingarten
43ac8d560b
Freeze all the things
2014-01-07 12:35:16 -05:00
James MacAulay
11dc18bfdf
A better fix for "and"/"or" in strings
...
(now with less side effects)
2009-09-23 15:44:29 -04:00
James MacAulay
f42ce88456
fixed conditions with strings containing "and"/"or"
2009-09-14 15:01:26 -04:00
James MacAulay
d1d6febfc1
'contains' operator returns false if either operand is nil
2009-08-19 19:38:31 -04:00
Jakub Kuźma and Tobias Lütke
8d27864845
Ruby 1.9.1 bugfixes
...
Signed-off-by: Tobias Lütke <[email protected] >
2009-04-17 06:33:25 +08:00
Tobias Lütke
7f58cbf82d
Merged last set of changes from original SVN location
2008-05-08 11:34:43 -04:00
Tobias Lütke
1d647361e1
Initial github import of liquid
2008-05-08 11:28:13 -04:00