From 33bac87a5cf4192ce478162849465cbde1f15bc5 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:14:21 -0500 Subject: [PATCH 01/25] 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 --- lib/liquid/condition.rb | 62 +++++++-- lib/liquid/standardfilters.rb | 2 + lib/liquid/variable_lookup.rb | 4 + test/integration/standard_filter_test.rb | 32 +++++ test/unit/condition_unit_test.rb | 156 +++++++++++++++++++++++ 5 files changed, 246 insertions(+), 10 deletions(-) diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 9ab350f07..2acf5ae35 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -113,24 +113,66 @@ def inspect def equal_variables(left, right) if left.is_a?(MethodLiteral) - if right.respond_to?(left.method_name) - return right.send(left.method_name) - else - return nil - end + return call_method_literal(left, right) end if right.is_a?(MethodLiteral) - if left.respond_to?(right.method_name) - return left.send(right.method_name) - else - return nil - end + return call_method_literal(right, left) end left == right end + def call_method_literal(literal, value) + method_name = literal.method_name + + # If the object responds to the method, use it + if value.respond_to?(method_name) + return value.send(method_name) + end + + # Implement blank?/empty? for common types that don't have it + # (ActiveSupport adds these, but Liquid should work without it) + case method_name + when :blank? + liquid_blank?(value) + when :empty? + liquid_empty?(value) + else + nil + end + end + + # Implement blank? semantics matching ActiveSupport + def liquid_blank?(value) + case value + when NilClass, FalseClass + true + when TrueClass, Numeric + false + when String + # Blank if empty or whitespace only + value.empty? || value.match?(/\A\s*\z/) + when Array, Hash + value.empty? + else + # Fall back to empty? if available, otherwise false + value.respond_to?(:empty?) ? value.empty? : false + end + end + + # Implement empty? semantics + def liquid_empty?(value) + case value + when NilClass + true + when String, Array, Hash + value.empty? + else + value.respond_to?(:empty?) ? value.empty? : false + end + end + def interpret_condition(left, right, op, context) # If the operator is empty this means that the decision statement is just # a single variable. We can just poll this variable from the context and diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 6e072fcf5..d17a699c6 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -768,6 +768,7 @@ def date(input, format) # @liquid_syntax array | first # @liquid_return [untyped] def first(array) + return array[0] if array.is_a?(String) array.first if array.respond_to?(:first) end @@ -779,6 +780,7 @@ def first(array) # @liquid_syntax array | last # @liquid_return [untyped] def last(array) + return array[-1] if array.is_a?(String) array.last if array.respond_to?(:last) end diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb index 340c0b66d..bc9bbd975 100644 --- a/lib/liquid/variable_lookup.rb +++ b/lib/liquid/variable_lookup.rb @@ -70,6 +70,10 @@ def evaluate(context) elsif lookup_command?(i) && object.respond_to?(key) object = object.send(key).to_liquid + # Handle string first/last like ActiveSupport does (returns first/last character) + elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last") + object = key == "first" ? object[0] : object[-1] + # No key was present with the desired value and it wasn't one of the directly supported # keywords either. The only thing we got left is to return nil or # raise an exception if `strict_variables` option is set to true diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index e27605ff5..eb95276c7 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -627,6 +627,38 @@ def test_first_last assert_nil(@filters.last([])) end + def test_first_last_on_strings + # Ruby's String class does not have first/last methods by default. + # ActiveSupport adds String#first and String#last to return the first/last character. + # Liquid must work without ActiveSupport, so the first/last filters handle strings specially. + # + # This enables template patterns like: + # {{ product.title | first }} => "S" (for "Snowboard") + # {{ customer.name | last }} => "h" (for "Smith") + assert_equal('f', @filters.first('foo')) + assert_equal('o', @filters.last('foo')) + assert_nil(@filters.first('')) + assert_nil(@filters.last('')) + end + + def test_first_last_on_unicode_strings + # Unicode strings should return the first/last grapheme cluster (character), + # not the first/last byte. Ruby's String#[] handles this correctly with index 0/-1. + # This ensures international text works properly: + # {{ korean_name | first }} => "고" (not a partial byte sequence) + assert_equal('고', @filters.first('고스트빈')) + assert_equal('빈', @filters.last('고스트빈')) + end + + def test_first_last_on_strings_via_template + # Integration test to verify the filter works end-to-end in templates. + # Empty strings return empty output (nil renders as empty string). + assert_template_result('f', '{{ name | first }}', { 'name' => 'foo' }) + assert_template_result('o', '{{ name | last }}', { 'name' => 'foo' }) + assert_template_result('', '{{ name | first }}', { 'name' => '' }) + assert_template_result('', '{{ name | last }}', { 'name' => '' }) + end + def test_replace assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b')) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2)) diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb index eb466f7a9..e250ce0f5 100644 --- a/test/unit/condition_unit_test.rb +++ b/test/unit/condition_unit_test.rb @@ -197,6 +197,162 @@ def test_parse_expression_with_safe_true_in_strict2_mode assert_equal(['title'], result.lookups) end + # Tests for blank? comparison without ActiveSupport + # + # Ruby's standard library does not include blank? on String, Array, Hash, etc. + # ActiveSupport adds blank? but Liquid must work without it. These tests verify + # that Liquid implements blank? semantics internally for use in templates like: + # {% if x == blank %}...{% endif %} + # + # The blank? semantics match ActiveSupport's behavior: + # - nil and false are blank + # - Strings are blank if empty or contain only whitespace + # - Arrays and Hashes are blank if empty + # - true and numbers are never blank + + def test_blank_with_whitespace_string + # Template authors expect " " to be blank since it has no visible content. + # This matches ActiveSupport's String#blank? which returns true for whitespace-only strings. + @context['whitespace'] = ' ' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('whitespace'), '==', blank_literal) + end + + def test_blank_with_empty_string + # An empty string has no content, so it should be considered blank. + # This is the most basic case of a blank string. + @context['empty_string'] = '' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_string'), '==', blank_literal) + end + + def test_blank_with_empty_array + # Empty arrays have no elements, so they are blank. + # Useful for checking if a collection has items: {% if products == blank %} + @context['empty_array'] = [] + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_array'), '==', blank_literal) + end + + def test_blank_with_empty_hash + # Empty hashes have no key-value pairs, so they are blank. + # Useful for checking if settings/options exist: {% if settings == blank %} + @context['empty_hash'] = {} + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('empty_hash'), '==', blank_literal) + end + + def test_blank_with_nil + # nil represents "nothing" and is the canonical blank value. + # Unassigned variables resolve to nil, so this enables: {% if missing_var == blank %} + @context['nil_value'] = nil + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('nil_value'), '==', blank_literal) + end + + def test_blank_with_false + # false is considered blank to match ActiveSupport semantics. + # This allows {% if some_flag == blank %} to work when flag is false. + @context['false_value'] = false + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_true(VariableLookup.new('false_value'), '==', blank_literal) + end + + def test_not_blank_with_true + # true is a definite value, not blank. + # Ensures {% if flag == blank %} works correctly for boolean flags. + @context['true_value'] = true + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('true_value'), '==', blank_literal) + end + + def test_not_blank_with_number + # Numbers (including zero) are never blank - they represent actual values. + # 0 is a valid quantity, not the absence of a value. + @context['number'] = 42 + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('number'), '==', blank_literal) + end + + def test_not_blank_with_string_content + # A string with actual content is not blank. + # This is the expected behavior for most template string comparisons. + @context['string'] = 'hello' + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('string'), '==', blank_literal) + end + + def test_not_blank_with_non_empty_array + # An array with elements has content, so it's not blank. + # Enables patterns like {% unless products == blank %}Show products{% endunless %} + @context['array'] = [1, 2, 3] + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('array'), '==', blank_literal) + end + + def test_not_blank_with_non_empty_hash + # A hash with key-value pairs has content, so it's not blank. + # Useful for checking if configuration exists: {% if config != blank %} + @context['hash'] = { 'a' => 1 } + blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] + + assert_evaluates_false(VariableLookup.new('hash'), '==', blank_literal) + end + + # Tests for empty? comparison without ActiveSupport + # + # empty? is distinct from blank? - it only checks if a collection has zero elements. + # For strings, empty? checks length == 0, NOT whitespace content. + # Ruby's standard library has empty? on String, Array, and Hash, but Liquid + # provides a fallback implementation for consistency. + + def test_empty_with_empty_string + # An empty string ("") has length 0, so it's empty. + # Different from blank - empty is a stricter check. + @context['empty_string'] = '' + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_string'), '==', empty_literal) + end + + def test_empty_with_whitespace_string_not_empty + # Whitespace strings have length > 0, so they are NOT empty. + # This is the key difference between empty and blank: + # " ".empty? => false, but " ".blank? => true + @context['whitespace'] = ' ' + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_false(VariableLookup.new('whitespace'), '==', empty_literal) + end + + def test_empty_with_empty_array + # An array with no elements is empty. + # [].empty? => true + @context['empty_array'] = [] + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_array'), '==', empty_literal) + end + + def test_empty_with_empty_hash + # A hash with no key-value pairs is empty. + # {}.empty? => true + @context['empty_hash'] = {} + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal) + end + private def assert_evaluates_true(left, op, right) From 0e3548d39ec28ec1cb843b44e8503a9a433b2055 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:16:24 -0500 Subject: [PATCH 02/25] Remove redundant else-clause --- lib/liquid/condition.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 2acf5ae35..16eca6d68 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -138,8 +138,6 @@ def call_method_literal(literal, value) liquid_blank?(value) when :empty? liquid_empty?(value) - else - nil end end From 0ed29760c07cc20504aa46463185bfffa318396d Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:18:35 -0500 Subject: [PATCH 03/25] Add benchmark gem for Ruby 4.0 compatibility --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index a404c0d4b..4cb905638 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,7 @@ group :development do end group :test do + gem 'benchmark' gem 'rubocop', '~> 1.61.0' gem 'rubocop-shopify', '~> 2.12.0', require: false gem 'rubocop-performance', require: false From 391c0df57ae7033bac09277c9ff2b31da37f0c76 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:19:21 -0500 Subject: [PATCH 04/25] Update rubocop to 1.82.0 for Ruby 4.0 support --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 4cb905638..9bab0a360 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ end group :test do gem 'benchmark' - gem 'rubocop', '~> 1.61.0' + gem 'rubocop', '~> 1.82.0' gem 'rubocop-shopify', '~> 2.12.0', require: false gem 'rubocop-performance', require: false end From 361d1d52b167d83f5f9a94c659d40643ab9a0012 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:20:50 -0500 Subject: [PATCH 05/25] Fix rubocop offenses from 1.82 upgrade --- lib/liquid/expression.rb | 2 +- lib/liquid/i18n.rb | 2 +- lib/liquid/tokenizer.rb | 2 +- test/integration/security_test.rb | 4 ++-- test/integration/standard_filter_test.rb | 2 +- test/integration/template_test.rb | 2 +- test/unit/strainer_template_unit_test.rb | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/liquid/expression.rb b/lib/liquid/expression.rb index 2605c5576..00c40a4c3 100644 --- a/lib/liquid/expression.rb +++ b/lib/liquid/expression.rb @@ -55,7 +55,7 @@ def parse(markup, ss = StringScanner.new(""), cache = nil) end def inner_parse(markup, ss, cache) - if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX + if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX return RangeLookup.parse( Regexp.last_match(1), Regexp.last_match(2), diff --git a/lib/liquid/i18n.rb b/lib/liquid/i18n.rb index 4a2885e86..982688115 100644 --- a/lib/liquid/i18n.rb +++ b/lib/liquid/i18n.rb @@ -28,7 +28,7 @@ def locale def interpolate(name, vars) name.gsub(/%\{(\w+)\}/) do # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] - (vars[Regexp.last_match(1).to_sym]).to_s + vars[Regexp.last_match(1).to_sym].to_s end end diff --git a/lib/liquid/tokenizer.rb b/lib/liquid/tokenizer.rb index 56dd218ec..8b331d93c 100644 --- a/lib/liquid/tokenizer.rb +++ b/lib/liquid/tokenizer.rb @@ -117,7 +117,7 @@ def next_variable_token byte_a = byte_b = @ss.scan_byte while byte_b - byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY) + byte_a = @ss.scan_byte while byte_a && byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY break unless byte_a diff --git a/test/integration/security_test.rb b/test/integration/security_test.rb index 75f092965..f84058940 100644 --- a/test/integration/security_test.rb +++ b/test/integration/security_test.rb @@ -59,7 +59,7 @@ def test_does_not_permanently_add_filters_to_symbol_table GC.start - assert_equal([], (Symbol.all_symbols - current_symbols)) + assert_equal([], Symbol.all_symbols - current_symbols) end def test_does_not_add_drop_methods_to_symbol_table @@ -70,7 +70,7 @@ def test_does_not_add_drop_methods_to_symbol_table assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) - assert_equal([], (Symbol.all_symbols - current_symbols)) + assert_equal([], Symbol.all_symbols - current_symbols) end def test_max_depth_nested_blocks_does_not_raise_exception diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index eb95276c7..4a10716d7 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -1328,7 +1328,7 @@ def test_sum_with_non_string_property assert_equal(1, @filters.sum(input, true)) assert_equal(0.2, @filters.sum(input, 1.0)) assert_equal(-0.3, @filters.sum(input, 1)) - assert_equal(0.4, @filters.sum(input, (1..5))) + assert_equal(0.4, @filters.sum(input, 1..5)) assert_equal(0, @filters.sum(input, nil)) assert_equal(0, @filters.sum(input, "")) end diff --git a/test/integration/template_test.rb b/test/integration/template_test.rb index 92ece017c..c01c7161c 100644 --- a/test/integration/template_test.rb +++ b/test/integration/template_test.rb @@ -133,7 +133,7 @@ def test_resource_limits_render_score assert(t.resource_limits.reached?) t.resource_limits.render_score_limit = 200 - assert_equal((" foo " * 100), t.render!) + assert_equal(" foo " * 100, t.render!) refute_nil(t.resource_limits.render_score) end diff --git a/test/unit/strainer_template_unit_test.rb b/test/unit/strainer_template_unit_test.rb index a04107084..aa3f153a3 100644 --- a/test/unit/strainer_template_unit_test.rb +++ b/test/unit/strainer_template_unit_test.rb @@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test def test_add_filter_when_wrong_filter_class c = Context.new s = c.strainer - wrong_filter = ->(v) { v.reverse } + wrong_filter = lambda(&:reverse) exception = assert_raises(TypeError) do s.class.add_filter(wrong_filter) From af58800c1605a9c0033a50b3a22897f8003e979e Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 20:22:19 -0500 Subject: [PATCH 06/25] Update rubocop-shopify to 2.18.0 and fix new offenses --- .rubocop_todo.yml | 11 ++++++++++- Gemfile | 2 +- lib/liquid/condition.rb | 4 ++-- lib/liquid/drop.rb | 2 +- lib/liquid/utils.rb | 2 +- test/integration/standard_filter_test.rb | 2 +- test/integration/tags/standard_tag_test.rb | 2 +- test/unit/condition_unit_test.rb | 4 ++-- 8 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 424b9b4d3..1fcb7b54c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -174,7 +174,16 @@ Style/WordArray: # Offense count: 117 # This cop supports safe auto-correction (--auto-correct). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: Max: 260 + +Naming/PredicatePrefix: + Enabled: false + +# Offense count: 1 +# This is intentional - early return from begin/rescue in assignment context +Lint/NoReturnInBeginEndBlocks: + Exclude: + - 'lib/liquid/standardfilters.rb' diff --git a/Gemfile b/Gemfile index 9bab0a360..07994975c 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,6 @@ end group :test do gem 'benchmark' gem 'rubocop', '~> 1.82.0' - gem 'rubocop-shopify', '~> 2.12.0', require: false + gem 'rubocop-shopify', '~> 2.18.0', require: false gem 'rubocop-performance', require: false end diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 16eca6d68..a4096551c 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -194,8 +194,8 @@ def interpret_condition(left, right, op, context) end def deprecated_default_context - warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \ - " and will be removed from Liquid 6.0.0.") + warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \ + "and will be removed from Liquid 6.0.0.") Context.new end diff --git a/lib/liquid/drop.rb b/lib/liquid/drop.rb index b990630e4..d13398ec4 100644 --- a/lib/liquid/drop.rb +++ b/lib/liquid/drop.rb @@ -31,7 +31,7 @@ def initialize # Catch all for the method def liquid_method_missing(method) - return nil unless @context&.strict_variables + return unless @context&.strict_variables raise Liquid::UndefinedDropMethod, "undefined method #{method}" end diff --git a/lib/liquid/utils.rb b/lib/liquid/utils.rb index eb0a02a9f..084739a21 100644 --- a/lib/liquid/utils.rb +++ b/lib/liquid/utils.rb @@ -69,7 +69,7 @@ def self.to_date(obj) return obj if obj.respond_to?(:strftime) if obj.is_a?(String) - return nil if obj.empty? + return if obj.empty? obj = obj.downcase end diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index 4a10716d7..f81af489b 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -116,7 +116,7 @@ def test_slice end def test_slice_on_arrays - input = 'foobar'.split(//) + input = 'foobar'.split('') assert_equal(%w(o o b), @filters.slice(input, 1, 3)) assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000)) assert_equal(%w(), @filters.slice(input, 1, 0)) diff --git a/test/integration/tags/standard_tag_test.rb b/test/integration/tags/standard_tag_test.rb index 6c297d407..67471e735 100644 --- a/test/integration/tags/standard_tag_test.rb +++ b/test/integration/tags/standard_tag_test.rb @@ -117,7 +117,7 @@ def test_case assigns = { 'condition' => "bad string here" } assert_template_result( '', - '{% case condition %}{% when "string here" %} hit {% endcase %}',\ + '{% case condition %}{% when "string here" %} hit {% endcase %}', assigns, ) end diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb index e250ce0f5..2ac832759 100644 --- a/test/unit/condition_unit_test.rb +++ b/test/unit/condition_unit_test.rb @@ -161,8 +161,8 @@ def test_default_context_is_deprecated assert_equal(true, Condition.new(1, '==', 1).evaluate) end - expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \ - " and will be removed from Liquid 6.0.0." + expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated " \ + "and will be removed from Liquid 6.0.0." assert_includes(err.lines.map(&:strip), expected) end From 05f9c2a0300d8fba14e1a26e94952c9f2c42e322 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:34:10 -0500 Subject: [PATCH 07/25] Add liquid-spec for conformance testing - Add liquid-spec gem from GitHub to :spec group - Create spec/ruby-liquid.rb adapter for the reference implementation - Add spec job to CI workflow to run liquid-spec tests --- .github/workflows/liquid.yml | 10 ++++++++++ Gemfile | 4 ++++ spec/ruby-liquid.rb | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 spec/ruby-liquid.rb diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index bd0df8e5b..ee204542e 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -42,6 +42,16 @@ jobs: env: RUBYOPT: ${{ matrix.entry.rubyopt }} + spec: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0 + with: + bundler-cache: true + bundler: latest + - run: bundle exec liquid-spec run spec/ruby-liquid.rb --no-max-failures + memory_profile: runs-on: ubuntu-latest steps: diff --git a/Gemfile b/Gemfile index 07994975c..05dab6514 100644 --- a/Gemfile +++ b/Gemfile @@ -30,3 +30,7 @@ group :test do gem 'rubocop-shopify', '~> 2.18.0', require: false gem 'rubocop-performance', require: false end + +group :spec do + gem 'liquid-spec', github: 'Shopify/liquid-spec' +end diff --git a/spec/ruby-liquid.rb b/spec/ruby-liquid.rb new file mode 100644 index 000000000..ebe782ba0 --- /dev/null +++ b/spec/ruby-liquid.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation) +# +# Run with: bundle exec liquid-spec run spec/ruby-liquid.rb + +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'liquid' + +LiquidSpec.configure do |config| + # Run core Liquid specs + config.features = [:core] +end + +# Compile a template string into a Liquid::Template +LiquidSpec.compile do |source, options| + Liquid::Template.parse(source, **options) +end + +# Render a compiled template with the given context +LiquidSpec.render do |template, ctx| + static_registers = ctx.registers + registers = Liquid::Registers.new(static_registers) + + context = Liquid::Context.build( + static_environments: ctx.environment, + registers: registers, + rethrow_errors: ctx.rethrow_errors? + ) + + context.exception_renderer = ctx.exception_renderer if ctx.exception_renderer + + template.render(context) +end From 533d470723ce234de824534857f225236cf2dcbf Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:35:13 -0500 Subject: [PATCH 08/25] Fix spec job to include :spec bundle group --- .github/workflows/liquid.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index ee204542e..821ccda5e 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -44,6 +44,8 @@ jobs: spec: runs-on: ubuntu-latest + env: + BUNDLE_WITH: spec steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0 From 34c274d3149cedd4fea250cb7e687d7dbb05ef5b Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:42:11 -0500 Subject: [PATCH 09/25] Fix rubocop: rename ruby-liquid.rb to ruby_liquid.rb and add trailing comma --- .github/workflows/liquid.yml | 2 +- spec/{ruby-liquid.rb => ruby_liquid.rb} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename spec/{ruby-liquid.rb => ruby_liquid.rb} (88%) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 821ccda5e..6fea0ba60 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -52,7 +52,7 @@ jobs: with: bundler-cache: true bundler: latest - - run: bundle exec liquid-spec run spec/ruby-liquid.rb --no-max-failures + - run: bundle exec liquid-spec run spec/ruby_liquid.rb --no-max-failures memory_profile: runs-on: ubuntu-latest diff --git a/spec/ruby-liquid.rb b/spec/ruby_liquid.rb similarity index 88% rename from spec/ruby-liquid.rb rename to spec/ruby_liquid.rb index ebe782ba0..d82d21da9 100644 --- a/spec/ruby-liquid.rb +++ b/spec/ruby_liquid.rb @@ -2,7 +2,7 @@ # Liquid Spec Adapter for Shopify/liquid (Ruby reference implementation) # -# Run with: bundle exec liquid-spec run spec/ruby-liquid.rb +# Run with: bundle exec liquid-spec run spec/ruby_liquid.rb $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require 'liquid' @@ -25,7 +25,7 @@ context = Liquid::Context.build( static_environments: ctx.environment, registers: registers, - rethrow_errors: ctx.rethrow_errors? + rethrow_errors: ctx.rethrow_errors?, ) context.exception_renderer = ctx.exception_renderer if ctx.exception_renderer From 19528a9b3f3c3841a6e19a7b375a269775b508ad Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:42:47 -0500 Subject: [PATCH 10/25] Update CI matrix: remove Ruby 3.0-3.2, add Ruby 4.0 --- .github/workflows/liquid.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 6fea0ba60..b57313613 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -11,17 +11,15 @@ jobs: strategy: matrix: entry: - - { ruby: 3.0, allowed-failure: false } # minimum supported - - { ruby: 3.2, allowed-failure: false } - - { ruby: 3.3, allowed-failure: false } - - { ruby: 3.3, allowed-failure: false } - - { ruby: 3.4, allowed-failure: false } # latest + - { ruby: 3.3, allowed-failure: false } # minimum supported + - { ruby: 3.4, allowed-failure: false } + - { ruby: 4.0, allowed-failure: false } # latest - { - ruby: 3.4, + ruby: 4.0, allowed-failure: false, rubyopt: "--enable-frozen-string-literal", } - - { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" } - { ruby: head, allowed-failure: false } - { ruby: head, From e0b46049afc9452e43564883282f99e7d1ccf7ec Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:44:28 -0500 Subject: [PATCH 11/25] Add Ruby 3.4 yjit, 4.0 zjit, and head zjit to CI matrix --- .github/workflows/liquid.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index b57313613..23b82708b 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -13,13 +13,15 @@ jobs: entry: - { ruby: 3.3, allowed-failure: false } # minimum supported - { ruby: 3.4, allowed-failure: false } - - { ruby: 4.0, allowed-failure: false } # latest + - { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: 4.0, allowed-failure: false } # latest stable - { ruby: 4.0, allowed-failure: false, rubyopt: "--enable-frozen-string-literal", } - { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" } - { ruby: head, allowed-failure: false } - { ruby: head, @@ -27,6 +29,7 @@ jobs: rubyopt: "--enable-frozen-string-literal", } - { ruby: head, allowed-failure: false, rubyopt: "--yjit" } + - { ruby: head, allowed-failure: false, rubyopt: "--zjit" } name: Test Ruby ${{ matrix.entry.ruby }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 From 7e3ccbc1886d869dcb26456533c0f5b8e4f4be6d Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:49:33 -0500 Subject: [PATCH 12/25] test --- .github/workflows/liquid.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 23b82708b..c645e9214 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -12,7 +12,6 @@ jobs: matrix: entry: - { ruby: 3.3, allowed-failure: false } # minimum supported - - { ruby: 3.4, allowed-failure: false } - { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" } - { ruby: 4.0, allowed-failure: false } # latest stable - { @@ -51,7 +50,7 @@ jobs: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0 with: - bundler-cache: true + #bundler-cache: true bundler: latest - run: bundle exec liquid-spec run spec/ruby_liquid.rb --no-max-failures From f4890de9d5e845285d7eac55ec3f2651ef7764df Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:54:08 -0500 Subject: [PATCH 13/25] hm --- .github/workflows/liquid.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index c645e9214..1d27600fe 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -21,14 +21,16 @@ jobs: } - { ruby: 4.0, allowed-failure: false, rubyopt: "--yjit" } - { ruby: 4.0, allowed-failure: false, rubyopt: "--zjit" } - - { ruby: head, allowed-failure: false } + + # Head can have failures due to being in development + - { ruby: head, allowed-failure: true } - { ruby: head, - allowed-failure: false, + allowed-failure: true, rubyopt: "--enable-frozen-string-literal", } - - { ruby: head, allowed-failure: false, rubyopt: "--yjit" } - - { ruby: head, allowed-failure: false, rubyopt: "--zjit" } + - { ruby: head, allowed-failure: true, rubyopt: "--yjit" } + - { ruby: head, allowed-failure: true, rubyopt: "--zjit" } name: Test Ruby ${{ matrix.entry.ruby }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 @@ -50,7 +52,7 @@ jobs: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - uses: ruby/setup-ruby@a25f1e45f0e65a92fcb1e95e8847f78fb0a7197a # v1.273.0 with: - #bundler-cache: true + bundler-cache: true bundler: latest - run: bundle exec liquid-spec run spec/ruby_liquid.rb --no-max-failures From ccd10a986a0ef43b68a50f24e234cde806df3568 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:56:39 -0500 Subject: [PATCH 14/25] Pin liquid-spec to main branch --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 05dab6514..3aa8c44f1 100644 --- a/Gemfile +++ b/Gemfile @@ -32,5 +32,5 @@ group :test do end group :spec do - gem 'liquid-spec', github: 'Shopify/liquid-spec' + gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main' end From 53641e19ce2682847e3b616126a706a3c2efb5bc Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:57:34 -0500 Subject: [PATCH 15/25] Pin liquid-spec to minimum required commit 3d1b492 --- Gemfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 3aa8c44f1..47ffd8760 100644 --- a/Gemfile +++ b/Gemfile @@ -32,5 +32,6 @@ group :test do end group :spec do - gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main' + # Minimum required: 3d1b492 (run `bundle update liquid-spec` to get newer) + gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '3d1b492dce27cd78b4d5a46ca8cf260ae1349f29' end From d321adae77f926e74b7ac5820a352fff47e7fa05 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 21:59:16 -0500 Subject: [PATCH 16/25] Fix spec adapter for liquid-spec API (template, assigns, options) --- spec/ruby_liquid.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/ruby_liquid.rb b/spec/ruby_liquid.rb index d82d21da9..5eda72da4 100644 --- a/spec/ruby_liquid.rb +++ b/spec/ruby_liquid.rb @@ -18,17 +18,19 @@ end # Render a compiled template with the given context -LiquidSpec.render do |template, ctx| - static_registers = ctx.registers - registers = Liquid::Registers.new(static_registers) +# @param template [Liquid::Template] compiled template +# @param assigns [Hash] environment variables +# @param options [Hash] :registers, :strict_errors, :exception_renderer +LiquidSpec.render do |template, assigns, options| + registers = Liquid::Registers.new(options[:registers] || {}) context = Liquid::Context.build( - static_environments: ctx.environment, + static_environments: assigns, registers: registers, - rethrow_errors: ctx.rethrow_errors?, + rethrow_errors: options[:strict_errors], ) - context.exception_renderer = ctx.exception_renderer if ctx.exception_renderer + context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] template.render(context) end From 608a877053e76e31c2829426b257aa2c37032eee Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:00:38 -0500 Subject: [PATCH 17/25] Add spec adapter with ActiveSupport for comparison testing --- Gemfile | 1 + spec/ruby_liquid_with_active_support.rb | 37 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 spec/ruby_liquid_with_active_support.rb diff --git a/Gemfile b/Gemfile index 47ffd8760..c71645a8a 100644 --- a/Gemfile +++ b/Gemfile @@ -34,4 +34,5 @@ end group :spec do # Minimum required: 3d1b492 (run `bundle update liquid-spec` to get newer) gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '3d1b492dce27cd78b4d5a46ca8cf260ae1349f29' + gem 'activesupport' end diff --git a/spec/ruby_liquid_with_active_support.rb b/spec/ruby_liquid_with_active_support.rb new file mode 100644 index 000000000..2b3d817f8 --- /dev/null +++ b/spec/ruby_liquid_with_active_support.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Liquid Spec Adapter for Shopify/liquid with ActiveSupport loaded +# +# Run with: bundle exec liquid-spec run spec/ruby_liquid_with_active_support.rb + +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'active_support/all' +require 'liquid' + +LiquidSpec.configure do |config| + # Run core Liquid specs + config.features = [:core] +end + +# Compile a template string into a Liquid::Template +LiquidSpec.compile do |source, options| + Liquid::Template.parse(source, **options) +end + +# Render a compiled template with the given context +# @param template [Liquid::Template] compiled template +# @param assigns [Hash] environment variables +# @param options [Hash] :registers, :strict_errors, :exception_renderer +LiquidSpec.render do |template, assigns, options| + registers = Liquid::Registers.new(options[:registers] || {}) + + context = Liquid::Context.build( + static_environments: assigns, + registers: registers, + rethrow_errors: options[:strict_errors], + ) + + context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] + + template.render(context) +end From ae26cb29ac62c0c08fa58f30578a1de6e569ee16 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:00:49 -0500 Subject: [PATCH 18/25] Disable auto-require for activesupport gem --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index c71645a8a..304ebdf17 100644 --- a/Gemfile +++ b/Gemfile @@ -34,5 +34,5 @@ end group :spec do # Minimum required: 3d1b492 (run `bundle update liquid-spec` to get newer) gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '3d1b492dce27cd78b4d5a46ca8cf260ae1349f29' - gem 'activesupport' + gem 'activesupport', require: false end From b0fb0ad83fe24e153715378f0284cdceafe0353f Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:02:42 -0500 Subject: [PATCH 19/25] Run liquid-spec for all adapters in spec/*.rb --- .github/workflows/liquid.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 1d27600fe..0626ed56d 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -54,7 +54,12 @@ jobs: with: bundler-cache: true bundler: latest - - run: bundle exec liquid-spec run spec/ruby_liquid.rb --no-max-failures + - name: Run liquid-spec for all adapters + run: | + for adapter in spec/*.rb; do + echo "=== Running $adapter ===" + bundle exec liquid-spec run "$adapter" --no-max-failures + done memory_profile: runs-on: ubuntu-latest From ef13b2dfd5886f9af680129fbe2e4626ae54355f Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:06:22 -0500 Subject: [PATCH 20/25] 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 --- lib/liquid/condition.rb | 3 +-- lib/liquid/standardfilters.rb | 6 ++++-- lib/liquid/variable_lookup.rb | 3 ++- test/integration/standard_filter_test.rb | 6 ++++-- test/unit/condition_unit_test.rb | 10 ++++++++++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index a4096551c..eb674ced8 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -160,10 +160,9 @@ def liquid_blank?(value) end # Implement empty? semantics + # Note: nil is NOT empty (but IS blank). empty? checks if a collection has zero elements. def liquid_empty?(value) case value - when NilClass - true when String, Array, Hash value.empty? else diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index d17a699c6..24e135d91 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -768,7 +768,8 @@ def date(input, format) # @liquid_syntax array | first # @liquid_return [untyped] def first(array) - return array[0] if array.is_a?(String) + # ActiveSupport returns "" for empty strings, not nil + return array[0] || "" if array.is_a?(String) array.first if array.respond_to?(:first) end @@ -780,7 +781,8 @@ def first(array) # @liquid_syntax array | last # @liquid_return [untyped] def last(array) - return array[-1] if array.is_a?(String) + # ActiveSupport returns "" for empty strings, not nil + return array[-1] || "" if array.is_a?(String) array.last if array.respond_to?(:last) end diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb index bc9bbd975..4fba2a658 100644 --- a/lib/liquid/variable_lookup.rb +++ b/lib/liquid/variable_lookup.rb @@ -71,8 +71,9 @@ def evaluate(context) object = object.send(key).to_liquid # Handle string first/last like ActiveSupport does (returns first/last character) + # ActiveSupport returns "" for empty strings, not nil elsif lookup_command?(i) && object.is_a?(String) && (key == "first" || key == "last") - object = key == "first" ? object[0] : object[-1] + object = key == "first" ? (object[0] || "") : (object[-1] || "") # No key was present with the desired value and it wasn't one of the directly supported # keywords either. The only thing we got left is to return nil or diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index f81af489b..ef5f15493 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -635,10 +635,12 @@ def test_first_last_on_strings # This enables template patterns like: # {{ product.title | first }} => "S" (for "Snowboard") # {{ customer.name | last }} => "h" (for "Smith") + # + # Note: ActiveSupport returns "" for empty strings, not nil. assert_equal('f', @filters.first('foo')) assert_equal('o', @filters.last('foo')) - assert_nil(@filters.first('')) - assert_nil(@filters.last('')) + assert_equal('', @filters.first('')) + assert_equal('', @filters.last('')) end def test_first_last_on_unicode_strings diff --git a/test/unit/condition_unit_test.rb b/test/unit/condition_unit_test.rb index 2ac832759..d7f35f609 100644 --- a/test/unit/condition_unit_test.rb +++ b/test/unit/condition_unit_test.rb @@ -353,6 +353,16 @@ def test_empty_with_empty_hash assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal) end + def test_nil_is_not_empty + # nil is NOT empty - empty? checks if a collection has zero elements. + # nil is not a collection, so it cannot be empty. + # This differs from blank: nil IS blank, but nil is NOT empty. + @context['nil_value'] = nil + empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] + + assert_evaluates_false(VariableLookup.new('nil_value'), '==', empty_literal) + end + private def assert_evaluates_true(left, op, right) From 2988f1a5009b1b4947495a11fa7f055e0d33482f Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:15:38 -0500 Subject: [PATCH 21/25] Update liquid-spec to branch with per-spec required_features support --- Gemfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 304ebdf17..cdf23039c 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,6 @@ group :test do end group :spec do - # Minimum required: 3d1b492 (run `bundle update liquid-spec` to get newer) - gem 'liquid-spec', github: 'Shopify/liquid-spec', ref: '3d1b492dce27cd78b4d5a46ca8cf260ae1349f29' + gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'add-per-spec-required-features' gem 'activesupport', require: false end From ddee08fb95484d0134748949b71a083696905e0e Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:21:47 -0500 Subject: [PATCH 22/25] Add activesupport feature to with_active_support adapter Both adapters now pass with 0 failures: - ruby_liquid.rb: 4194 passed (skips activesupport and shopify_error_handling specs) - ruby_liquid_with_active_support.rb: 4203 passed (skips shopify_error_handling specs) --- spec/ruby_liquid_with_active_support.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ruby_liquid_with_active_support.rb b/spec/ruby_liquid_with_active_support.rb index 2b3d817f8..4a6f00570 100644 --- a/spec/ruby_liquid_with_active_support.rb +++ b/spec/ruby_liquid_with_active_support.rb @@ -9,8 +9,8 @@ require 'liquid' LiquidSpec.configure do |config| - # Run core Liquid specs - config.features = [:core] + # Run core Liquid specs plus ActiveSupport SafeBuffer tests + config.features = [:core, :activesupport] end # Compile a template string into a Liquid::Template From 79a2e042ff66e6dfb05314edce735fd19b05de03 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:24:00 -0500 Subject: [PATCH 23/25] Use liquid-spec main branch --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index cdf23039c..a6c448617 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,6 @@ group :test do end group :spec do - gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'add-per-spec-required-features' + gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main' gem 'activesupport', require: false end From 50e1789537edc78a3c8f53949d3dd4e8302df9b5 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:29:32 -0500 Subject: [PATCH 24/25] Use liquid-spec feature branch until PR is merged --- Gemfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index a6c448617..add479637 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ group :test do end group :spec do - gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main' + # Using feature branch until https://github.com/Shopify/liquid-spec/pull/97 is merged + gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'add-per-spec-required-features' gem 'activesupport', require: false end From 0058e4322ba0099206f9b263b9b80f25e2d1be30 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Thu, 1 Jan 2026 22:30:14 -0500 Subject: [PATCH 25/25] Add fail-fast: false to prevent job cancellation --- .github/workflows/liquid.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/liquid.yml b/.github/workflows/liquid.yml index 0626ed56d..de57dd68d 100644 --- a/.github/workflows/liquid.yml +++ b/.github/workflows/liquid.yml @@ -9,6 +9,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: entry: - { ruby: 3.3, allowed-failure: false } # minimum supported