Compare commits

..
Author SHA1 Message Date
Julia Boutin f78c443ccd Update resource limit handling
This commit aligns snippet tag's resource
limit handling with assign tag's `assign_score_of`
method where the snippet variable acts as a pointer
to a block body - increasing assign score by one
2025-11-04 14:53:08 -07:00
Julia Boutin e753b9025d Ensure render tag checks for most common type first
Previously, render tag would check if a template
responded to `:to_partial` before checking if its
name was a string. As `is_a?(String)` instances are
more common that `to_partial`, this commit reorders
the conditional to follow the common path
2025-11-04 13:11:18 -07:00
63 changed files with 1469 additions and 1531 deletions
+15 -36
View File
@@ -1,5 +1,5 @@
name: Liquid
on: [push]
on: [push, pull_request]
env:
BUNDLE_JOBS: 4
@@ -9,34 +9,30 @@ jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
entry:
- { ruby: 3.3, allowed-failure: false } # minimum supported
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: 4.0, allowed-failure: false } # latest stable
- { 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: 4.0,
ruby: 3.4,
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: truffleruby, allowed-failure: false }
# Head can have failures due to being in development
- { ruby: head, allowed-failure: true }
- { ruby: 3.4, allowed-failure: false, rubyopt: "--yjit" }
- { ruby: ruby-head, allowed-failure: false }
- {
ruby: head,
allowed-failure: true,
ruby: ruby-head,
allowed-failure: false,
rubyopt: "--enable-frozen-string-literal",
}
- { ruby: head, allowed-failure: true, rubyopt: "--yjit" }
- { ruby: head, allowed-failure: true, rubyopt: "--zjit" }
name: Test Ruby ${{ matrix.entry.ruby }} ${{ matrix.entry.rubyopt }} --${{ matrix.entry.allowed-failure && 'allowed-failure' || 'strict' }}
- { ruby: ruby-head, allowed-failure: false, rubyopt: "--yjit" }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
@@ -46,28 +42,11 @@ jobs:
env:
RUBYOPT: ${{ matrix.entry.rubyopt }}
spec:
runs-on: ubuntu-latest
env:
BUNDLE_WITH: spec
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
bundler-cache: true
bundler: latest
- 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
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
- uses: ruby/setup-ruby@dffc446db9ba5a0c4446edb5bca1c5c473a806c5 # v1.235.0
with:
bundler-cache: true
- run: bundle exec rake memory_profile:run
+1 -10
View File
@@ -174,16 +174,7 @@ Style/WordArray:
# Offense count: 117
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, AllowCopDirectives, AllowedPatterns.
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
# 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'
+2 -8
View File
@@ -25,13 +25,7 @@ group :development do
end
group :test do
gem 'benchmark'
gem 'rubocop', '~> 1.82.0'
gem 'rubocop-shopify', '~> 2.18.0', require: false
gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false
end
group :spec do
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main'
gem 'activesupport', require: false
end
+6 -4
View File
@@ -1,11 +1,13 @@
# Liquid Change Log
## 5.11.0
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
* Rename the `:rigid` error mode to `:strict2` and display a warning when users attempt to use the `:rigid` mode [Guilherme Carreiro]
## 5.10.0
* Introduce support for Inline Snippets [Julia Boutin]
```
{%- snippet snowdevil -%}
Snowdevil
{%- endsnippet -%}
{% render snowdevil %}
```
## 5.9.0
* Introduce `:rigid` error mode for stricter, safer parsing of all tags [CP Clermont, Guilherme Carreiro]
+4 -4
View File
@@ -103,10 +103,10 @@ Liquid also comes with different parsers that can be used when editing templates
when templates are invalid. You can enable this new parser like this:
```ruby
Liquid::Environment.default.error_mode = :strict2 # Raises a SyntaxError when invalid syntax is used in all tags
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
Liquid::Environment.default.error_mode = :rigid # Raises a SyntaxError when invalid syntax is used in all tags
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
+8 -14
View File
@@ -33,7 +33,7 @@ task :rubocop do
end
end
desc('runs test suite with lax, strict, and strict2 parsers')
desc('runs test suite with lax, strict, and rigid parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
@@ -42,7 +42,7 @@ task :test do
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
ENV['LIQUID_PARSER_MODE'] = 'rigid'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
@@ -55,7 +55,7 @@ task :test do
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
ENV['LIQUID_PARSER_MODE'] = 'rigid'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
@@ -88,13 +88,13 @@ namespace :benchmark do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with strict2 parsing"
task :strict2 do
ruby "./performance/benchmark.rb strict2"
desc "Run the liquid benchmark with rigid parsing"
task :rigid do
ruby "./performance/benchmark.rb rigid"
end
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
task run: [:lax, :strict, :strict2]
desc "Run the liquid benchmark with lax, strict, and rigid parsing"
task run: [:lax, :strict, :rigid]
desc "Run unit benchmarks"
namespace :unit do
@@ -148,9 +148,3 @@ end
task :console do
exec 'irb -I lib -r liquid'
end
desc('run liquid-spec suite across all adapters')
task :spec do
adapters = Dir['./spec/*.rb'].join(',')
sh "bundle exec liquid-spec matrix --adapters=#{adapters} --reference=ruby_liquid"
end
+1 -1
View File
@@ -41,6 +41,6 @@ def assigns
end
puts Liquid::Template
.parse(source, error_mode: :strict2)
.parse(source, error_mode: :rigid)
.tap { |t| t.registers[:file_system] = VirtualFileSystem.new }
.render(assigns)
+1 -1
View File
@@ -52,7 +52,6 @@ end
require "liquid/version"
require "liquid/deprecations"
require "liquid/const"
require "liquid/byte_tables"
require 'liquid/standardfilters'
require 'liquid/file_system'
require 'liquid/parser_switching'
@@ -68,6 +67,7 @@ require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/snippet_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
+7 -83
View File
@@ -130,21 +130,16 @@ module Liquid
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
# rubocop:disable Metrics/BlockNesting
fast = try_parse_tag_token(token)
if fast
tag_name, markup, newlines = fast
elsif token =~ FullToken
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
newlines = parse_context.line_number ? Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n") : 0
else
unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block)
end
# rubocop:enable Metrics/BlockNesting
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
if parse_context.line_number && newlines > 0
parse_context.line_number += newlines
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
@@ -265,77 +260,6 @@ module Liquid
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
# Fast path for parsing "{%[-] tag_name markup [-]%}" tag tokens.
# Returns [tag_name, markup, newline_count] or nil.
#
# Accepts tokens where:
# - Tag name is '#' or starts with [a-zA-Z_] followed by \w chars
# (matching TagName = /#|\w+/ exactly — no hyphens, no '?' suffix)
# - Whitespace is spaces, tabs, newlines, \r, \f, \v
# - Whitespace control dashes are at positions 2 and len-3
# Rejects (returns nil → caller falls back to FullToken regex):
# - Tokens shorter than "{%x%}" (4 bytes)
# - Tag names starting with a digit (valid in FullToken but rare)
# - Any structure the byte-walk can't confidently parse
# Fallback: nil return triggers the original `token =~ FullToken` regex
# match in parse_for_document, preserving identical behavior for any
# input the fast path doesn't handle.
def try_parse_tag_token(token)
len = token.bytesize
pos = 2 # skip "{%"
return if pos >= len
pos += 1 if token.getbyte(pos) == ByteTables::DASH
newline_count = 0
# Skip whitespace before tag name, count newlines
while pos < len
b = token.getbyte(pos)
if b == ByteTables::NEWLINE
pos += 1
newline_count += 1
elsif ByteTables::WHITESPACE[b]
pos += 1
else
break
end
end
return if pos >= len
# Scan tag name: '#' or \w+ (matching TagName = /#|\w+/)
name_start = pos
b = token.getbyte(pos)
if b == ByteTables::HASH
pos += 1
elsif ByteTables::IDENT_START[b]
pos += 1
pos += 1 while pos < len && ByteTables::WORD[token.getbyte(pos)]
else
return
end
tag_name = token.byteslice(name_start, pos - name_start)
# Skip whitespace after tag name, count newlines
while pos < len
b = token.getbyte(pos)
if b == ByteTables::NEWLINE
pos += 1
newline_count += 1
elsif ByteTables::WHITESPACE[b]
pos += 1
else
break
end
end
# Markup: everything up to optional '-' before '%}'
markup_end = len - 2 # skip '%}'
markup_end -= 1 if markup_end > pos && token.getbyte(markup_end - 1) == ByteTables::DASH
markup = pos >= markup_end ? "" : token.byteslice(pos, markup_end - pos)
[tag_name, markup, newline_count]
end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context)
BlockBody.raise_missing_tag_terminator(token, parse_context)
-48
View File
@@ -1,48 +0,0 @@
# frozen_string_literal: true
module Liquid
# Pre-computed 256-entry boolean lookup tables for byte classification.
# Built once at load time; used as TABLE[byte] — a single array index
# instead of 3-5 comparison operators per check.
module ByteTables
# [a-zA-Z_] — valid first byte of an identifier
IDENT_START = Array.new(256, false).tap do |t|
(97..122).each { |b| t[b] = true } # a-z
(65..90).each { |b| t[b] = true } # A-Z
t[95] = true # _
end.freeze
# [a-zA-Z0-9_-] — valid continuation byte of an identifier
IDENT_CONT = Array.new(256, false).tap do |t|
(97..122).each { |b| t[b] = true } # a-z
(65..90).each { |b| t[b] = true } # A-Z
(48..57).each { |b| t[b] = true } # 0-9
t[95] = true # _
t[45] = true # -
end.freeze
# [a-zA-Z0-9_] — \w equivalent (no hyphen), for tag name scanning
WORD = Array.new(256, false).tap do |t|
(97..122).each { |b| t[b] = true } # a-z
(65..90).each { |b| t[b] = true } # A-Z
(48..57).each { |b| t[b] = true } # 0-9
t[95] = true # _
end.freeze
# [0-9] — ASCII digit
DIGIT = Array.new(256, false).tap do |t|
(48..57).each { |b| t[b] = true }
end.freeze
# Matches bytes removed by Ruby's String#strip: \x00, \t, \n, \v, \f, \r, space
WHITESPACE = Array.new(256, false).tap do |t|
[0, 9, 10, 11, 12, 13, 32].each { |b| t[b] = true }
end.freeze
# Byte constants for delimiters and punctuation
NEWLINE = 10
DASH = 45 # '-'
DOT = 46 # '.'
HASH = 35 # '#'
end
end
+12 -55
View File
@@ -113,67 +113,24 @@ module Liquid
def equal_variables(left, right)
if left.is_a?(MethodLiteral)
return call_method_literal(left, right)
if right.respond_to?(left.method_name)
return right.send(left.method_name)
else
return nil
end
end
if right.is_a?(MethodLiteral)
return call_method_literal(right, left)
if left.respond_to?(right.method_name)
return left.send(right.method_name)
else
return nil
end
end
left == right
end
def call_method_literal(literal, value)
method_name = literal.method_name
# If the object responds to the method (e.g., ActiveSupport is loaded), use it
if value.respond_to?(method_name)
value.send(method_name)
else
# Emulate ActiveSupport's blank?/empty? to make Liquid invariant
# to whether ActiveSupport is loaded or not
case method_name
when :blank?
liquid_blank?(value)
when :empty?
liquid_empty?(value)
else
false
end
end
end
# Implement blank? semantics matching ActiveSupport
# blank? returns true for nil, false, empty strings, whitespace-only strings,
# empty arrays, and empty hashes
def liquid_blank?(value)
case value
when NilClass, FalseClass
true
when TrueClass, Numeric
false
when String
# Blank if empty or whitespace only (matches ActiveSupport)
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
# Note: nil is NOT empty. empty? checks if a collection has zero elements.
def liquid_empty?(value)
case value
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
@@ -197,8 +154,8 @@ module Liquid
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
+1 -1
View File
@@ -184,7 +184,7 @@ module Liquid
end
def key?(key)
find_variable(key, raise_on_not_found: false) != nil
self[key] != nil
end
def evaluate(object)
+1 -1
View File
@@ -31,7 +31,7 @@ module Liquid
# Catch all for the method
def liquid_method_missing(method)
return unless @context&.strict_variables
return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
+1 -1
View File
@@ -34,7 +34,7 @@ module Liquid
# @param file_system The default file system that is used
# to load templates from.
# @param error_mode [Symbol] The default error mode for all templates
# (either :strict2, :strict, :warn, or :lax).
# (either :rigid, :strict, :warn, or :lax).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
+50 -69
View File
@@ -16,9 +16,16 @@ module Liquid
'-' => VariableLookup.parse("-", nil).freeze,
}.freeze
DOT = ".".ord
ZERO = "0".ord
NINE = "9".ord
DASH = "-".ord
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
@@ -28,15 +35,7 @@ module Liquid
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
# Guard: only call .strip when the first or last byte is whitespace.
# String#strip always allocates a new String, even when there's nothing
# to strip. ByteTables::WHITESPACE matches the same bytes that strip
# removes (space, \t, \n, \v, \f, \r, \x00). When neither end has
# whitespace, we skip the call and avoid ~4,464 allocations per compile.
first = markup.getbyte(0)
if first && (ByteTables::WHITESPACE[first] || ByteTables::WHITESPACE[markup.getbyte(markup.bytesize - 1)])
markup = markup.strip
end
markup = markup.strip # markup can be a frozen string
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
@@ -56,7 +55,7 @@ module Liquid
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),
@@ -72,74 +71,56 @@ module Liquid
end
end
# Fast path for number parsing. Accepts:
# - Simple integers: "42", "-7"
# - Simple floats: "3.14", "-0.5"
# - Multi-dot floats (truncated at second dot): "1.2.3" → 1.2
# - Trailing-dot floats: "123." → 123.0
# Rejects (returns nil → caller treats as VariableLookup):
# - Non-numeric input: "hello", ""
# - Inputs with non-digit/non-dot bytes after the number: "1.2.3a"
# Fallback: nil return causes caller to fall through to VariableLookup.parse,
# which is the same path the old regex-based code took on non-match.
def parse_number(markup, _ss = nil)
len = markup.bytesize
return if len == 0
pos = 0
first = markup.getbyte(pos)
if first == ByteTables::DASH
pos += 1
return if pos >= len
return unless ByteTables::DIGIT[markup.getbyte(pos)]
pos += 1
elsif ByteTables::DIGIT[first]
pos += 1
else
return
def parse_number(markup, ss)
# check if the markup is simple integer or float
case markup
when INTEGER_REGEX
return Integer(markup, 10)
when FLOAT_REGEX
return markup.to_f
end
# Scan digits
pos += 1 while pos < len && ByteTables::DIGIT[markup.getbyte(pos)]
ss.string = markup
# the first byte must be a digit or a dash
byte = ss.scan_byte
# Consumed everything = simple integer
return Integer(markup, 10) if pos == len
return false if byte != DASH && (byte < ZERO || byte > NINE)
# Check for dot — three float cases:
# 1. Simple float: "123.456" → markup.to_f
# 2. Multi-dot: "1.2.3.4" → truncate at second dot → 1.2
# 3. Trailing dot: "123." → truncate before dot → 123.0
return unless markup.getbyte(pos) == ByteTables::DOT
if byte == DASH
peek_byte = ss.peek_byte
dot_pos = pos
pos += 1
digit_start = pos
pos += 1 while pos < len && ByteTables::DIGIT[markup.getbyte(pos)]
# if it starts with a dash, the next byte must be a digit
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
end
if pos > digit_start && pos == len
# Case 1: simple float like "123.456"
markup.to_f
elsif pos > digit_start
# Case 2: multi-dot like "1.2.3.4" — find where the numeric
# portion ends. Reject if any non-digit, non-dot byte is found
# (e.g. "1.2.3a" → nil, matching the old regex-based behavior).
num_end = nil
check = pos
while check < len
b = markup.getbyte(check)
if b == ByteTables::DOT
num_end ||= check
elsif !ByteTables::DIGIT[b]
return
# The markup could be a float with multiple dots
first_dot_pos = nil
num_end_pos = nil
while (byte = ss.scan_byte)
return false if byte != DOT && (byte < ZERO || byte > NINE)
# we found our number and now we are just scanning the rest of the string
next if num_end_pos
if byte == DOT
if first_dot_pos.nil?
first_dot_pos = ss.pos
else
# we found another dot, so we know that the number ends here
num_end_pos = ss.pos - 1
end
check += 1
end
markup.byteslice(0, num_end || len).to_f
end
num_end_pos = markup.length if ss.eos?
if num_end_pos
# number ends with a number "123.123"
markup.byteslice(0, num_end_pos).to_f
else
# Case 3: trailing dot like "123."
markup.byteslice(0, dot_pos).to_f
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
end
end
end
+1 -1
View File
@@ -28,7 +28,7 @@ module Liquid
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
+3
View File
@@ -5,6 +5,7 @@
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
@@ -19,6 +20,7 @@
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
@@ -29,5 +31,6 @@
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
argument:
include: "Argument error in tag 'include' - Illegal template name"
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
disabled:
tag: "usage is not allowed in this context"
+4 -4
View File
@@ -55,15 +55,15 @@ module Liquid
end
def parse_expression(markup, safe: false)
if !safe && @error_mode == :strict2
if !safe && @error_mode == :rigid
# parse_expression is a widely used API. To maintain backward
# compatibility while raising awareness about strict2 parser standards,
# compatibility while raising awareness about rigid parser standards,
# the safe flag supports API users make a deliberate decision.
#
# In strict2 mode, markup MUST come from a string returned by the parser
# In rigid mode, markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead.
raise Liquid::InternalError, "unsafe parse_expression cannot be used in strict2 mode"
raise Liquid::InternalError, "unsafe parse_expression cannot be used in rigid mode"
end
Expression.parse(markup, @string_scanner, @expression_cache)
+10 -18
View File
@@ -7,19 +7,16 @@ module Liquid
# It's basically doing the same thing the {#parse_with_selected_parser},
# except this will try the strict parser regardless of the error mode,
# and fall back to the lax parser if the error mode is lax or warn,
# except when in strict2 mode where it uses the strict2 parser.
# except when in rigid mode where it uses the rigid parser.
#
# @deprecated Use {#parse_with_selected_parser} instead.
def strict_parse_with_error_mode_fallback(markup)
return strict2_parse_with_error_context(markup) if strict2_mode?
return rigid_parse_with_error_context(markup) if rigid_mode?
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :rigid
rigid_warn
raise
when :strict2
raise
when :strict
raise
@@ -31,13 +28,12 @@ module Liquid
def parse_with_selected_parser(markup)
case parse_context.error_mode
when :rigid then rigid_warn && strict2_parse_with_error_context(markup)
when :strict2 then strict2_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :rigid then rigid_parse_with_error_context(markup)
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
strict2_parse_with_error_context(markup)
rigid_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
lax_parse(markup)
@@ -45,18 +41,14 @@ module Liquid
end
end
def strict2_mode?
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
def rigid_mode?
parse_context.error_mode == :rigid
end
private
def rigid_warn
Deprecations.warn(':rigid', ':strict2')
end
def strict2_parse_with_error_context(markup)
strict2_parse(markup)
def rigid_parse_with_error_context(markup)
rigid_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
+5 -22
View File
@@ -2,39 +2,24 @@
module Liquid
class ResourceLimits
attr_accessor :render_length_limit,
:render_score_limit,
:assign_score_limit,
:cumulative_render_score_limit,
:cumulative_assign_score_limit
attr_reader :render_score,
:assign_score,
:cumulative_render_score,
:cumulative_assign_score
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
attr_reader :render_score, :assign_score
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
@cumulative_render_score_limit = limits[:cumulative_render_score_limit]
@cumulative_assign_score_limit = limits[:cumulative_assign_score_limit]
@cumulative_render_score = 0
@cumulative_assign_score = 0
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
reset
end
def increment_render_score(amount)
@render_score += amount
@cumulative_render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
@cumulative_assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
@@ -62,8 +47,6 @@ module Liquid
@reached_limit = false
@last_capture_length = nil
@render_score = @assign_score = 0
raise_limits_reached if @cumulative_render_score_limit && @cumulative_render_score > @cumulative_render_score_limit
raise_limits_reached if @cumulative_assign_score_limit && @cumulative_assign_score > @cumulative_assign_score_limit
end
def with_capture
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Liquid
class SnippetDrop < Drop
attr_reader :body, :name, :filename
def initialize(body, name, filename)
super()
@body = body
@name = name
@filename = filename
end
def to_partial
@body
end
def to_s
'SnippetDrop'
end
end
end
+7 -33
View File
@@ -8,19 +8,10 @@ module Liquid
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
supports_64bit_indices = begin
[][1 << 33, 1 << 33]
true
rescue RangeError
false
end
INDEX_RANGE = if supports_64bit_indices
(-(1 << 63))..((1 << 63) - 1)
else
(-(1 << 31))..((1 << 31) - 1)
end
private_constant :INDEX_RANGE
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = {
'&' => '&amp;',
@@ -223,11 +214,11 @@ module Liquid
Utils.to_s(input).slice(offset, length) || ''
end
rescue RangeError
if INDEX_RANGE.cover?(length) && INDEX_RANGE.cover?(offset)
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(INDEX_RANGE)
length = length.clamp(INDEX_RANGE)
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end
end
@@ -302,19 +293,6 @@ module Liquid
input.split(pattern)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes leading and trailing whitespace and collapses consecutive whitespace to a single space.
# @liquid_syntax string | squish
# @liquid_return [string]
def squish(input)
return if input.nil?
Utils.to_s(input).strip.gsub(/\s+/, ' ')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
@@ -790,8 +768,6 @@ module Liquid
# @liquid_syntax array | first
# @liquid_return [untyped]
def first(array)
# ActiveSupport returns "" for empty strings, not nil
return array[0] || "" if array.is_a?(String)
array.first if array.respond_to?(:first)
end
@@ -803,8 +779,6 @@ module Liquid
# @liquid_syntax array | last
# @liquid_return [untyped]
def last(array)
# ActiveSupport returns "" for empty strings, not nil
return array[-1] || "" if array.is_a?(String)
array.last if array.respond_to?(:last)
end
+2
View File
@@ -20,6 +20,7 @@ require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
require_relative "tags/doc"
require_relative "tags/snippet"
module Liquid
module Tags
@@ -44,6 +45,7 @@ module Liquid
'echo' => Echo,
'tablerow' => TableRow,
'doc' => Doc,
'snippet' => Snippet,
}.freeze
end
end
+5 -5
View File
@@ -86,7 +86,7 @@ module Liquid
private
def strict2_parse(markup)
def rigid_parse(markup)
parser = @parse_context.new_parser(markup)
@left = safe_parse_expression(parser)
parser.consume(:end_of_string)
@@ -107,18 +107,18 @@ module Liquid
def record_when_condition(markup)
body = new_body
if strict2_mode?
parse_strict2_when(markup, body)
if rigid_mode?
parse_rigid_when(markup, body)
else
parse_lax_when(markup, body)
end
end
def parse_strict2_when(markup, body)
def parse_rigid_when(markup, body)
parser = @parse_context.new_parser(markup)
loop do
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
expr = safe_parse_expression(parser)
block = Condition.new(@left, '==', expr)
block.attach(body)
@blocks << block
+1 -1
View File
@@ -56,7 +56,7 @@ module Liquid
private
# cycle [name:] expression(, expression)*
def strict2_parse(markup)
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
@variables = []
+1 -1
View File
@@ -111,7 +111,7 @@ module Liquid
private
def strict2_parse(markup)
def rigid_parse(markup)
strict_parse(markup)
end
+1 -1
View File
@@ -66,7 +66,7 @@ module Liquid
private
def strict2_parse(markup)
def rigid_parse(markup)
strict_parse(markup)
end
+1 -1
View File
@@ -84,7 +84,7 @@ module Liquid
alias_method :parse_context, :options
private :parse_context
def strict2_parse(markup)
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = safe_parse_expression(p)
+23 -17
View File
@@ -27,7 +27,7 @@ module Liquid
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
@@ -47,21 +47,23 @@ module Liquid
end
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
template = context.evaluate(@template_name_expr)
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)
context_variable_name = @alias_name || template_name.split('/').last
if @template_name_expr.is_a?(String)
partial = PartialCache.load(template, context: context, parse_context: parse_context)
template_name = partial.name
context_variable_name = @alias_name || template_name.split('/').last
elsif template.respond_to?(:to_partial) && template.respond_to?(:name)
partial = template.to_partial
template_name = template.filename
context_variable_name = @alias_name || template.name
else
raise ::ArgumentError
end
render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name
inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@@ -85,10 +87,10 @@ module Liquid
end
# render (string) (with|for expression)? (as id)? (key: value)*
def strict2_parse(markup)
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = parse_expression(strict2_template_name(p), safe: true)
@template_name_expr = parse_expression(rigid_template_name(p), safe: true)
with_or_for = p.id?("for") || p.id?("with")
@variable_name_expr = safe_parse_expression(p) if with_or_for
@alias_name = p.consume(:id) if p.id?("as")
@@ -101,14 +103,18 @@ module Liquid
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
p.consume?(:comma) # optional comma
end
p.consume(:end_of_string)
end
def strict2_template_name(p)
p.consume(:string)
def rigid_template_name(p)
return p.consume(:string) if p.look(:string)
return p.consume(:id) if p.look(:id)
found = p.consume || "nothing"
raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found)
end
def strict_parse(markup)
+39
View File
@@ -0,0 +1,39 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name snippet
# @liquid_summary
# Creates a new inline snippet.
# @liquid_description
# You can create inline snippets to make your Liquid code more modular.
# @liquid_syntax
# {% snippet snippet_name %}
# value
# {% endsnippet %}
class Snippet < Block
def initialize(tag_name, markup, options)
super
p = @parse_context.new_parser(markup)
if p.look(:id)
@to = p.consume(:id)
p.consume(:end_of_string)
else
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end
end
def render_to_output_buffer(context, output)
snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
context.scopes.last[@to] = snippet_drop
context.resource_limits.increment_assign_score(1)
output
end
def blank?
true
end
end
end
+1 -1
View File
@@ -34,7 +34,7 @@ module Liquid
parse_with_selected_parser(markup)
end
def strict2_parse(markup)
def rigid_parse(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
+1 -1
View File
@@ -25,7 +25,7 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict enforces correct syntax for most tags
# :strict2 enforces correct syntax for all tags
# :rigid enforces correct syntax for all tags
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
+1 -1
View File
@@ -117,7 +117,7 @@ module Liquid
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
+1 -3
View File
@@ -69,7 +69,7 @@ module Liquid
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return if obj.empty?
return nil if obj.empty?
obj = obj.downcase
end
@@ -95,8 +95,6 @@ module Liquid
def self.to_s(obj, seen = {})
case obj
when BigDecimal
obj.to_s("F")
when Hash
# If the custom hash implementation overrides `#to_s`, use their
# custom implementation. Otherwise we use Liquid's default
+3 -3
View File
@@ -74,14 +74,14 @@ module Liquid
p.consume(:end_of_string)
end
def strict2_parse(markup)
def rigid_parse(markup)
@filters = []
p = @parse_context.new_parser(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
@filters << strict2_parse_filter_expressions(p) while p.consume?(:pipe)
@filters << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
p.consume(:end_of_string)
end
@@ -156,7 +156,7 @@ module Liquid
# argument = (positional_argument | keyword_argument)
# positional_argument = expression
# keyword_argument = id ":" expression
def strict2_parse_filter_expressions(p)
def rigid_parse_filter_expressions(p)
filtername = p.consume(:id)
filter_args = []
keyword_args = {}
-49
View File
@@ -4,25 +4,6 @@ module Liquid
class VariableLookup
COMMAND_METHODS = ['size', 'first', 'last'].freeze
# Matches simple identifier chains: name(.name)* with no brackets/quotes
SIMPLE_LOOKUP_RE = /\A[\w-]+\??(?:\.[\w-]+\??)*\z/
# Returns true when markup is a simple dotted identifier chain that the
# fast path in initialize can handle. Accepts:
# - Single names: "product", "item"
# - Dotted chains: "product.title", "cart.items.first"
# - Question-mark suffixes: "product.available?"
# - Hyphens in names: "my-var.some-field"
# Rejects (falls through to VariableParser regex):
# - Bracket lookups: "product[0]", "hash['key']"
# - Quoted strings, empty input, leading/trailing dots
# Fallback: when this returns false, initialize uses the original
# markup.scan(VariableParser) path — behavior is identical to
# the pre-optimization code for any input the fast path rejects.
def self.simple_lookup?(markup)
markup.bytesize > 0 && markup.match?(SIMPLE_LOOKUP_RE)
end
attr_reader :name, :lookups
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
@@ -30,31 +11,6 @@ module Liquid
end
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
if self.class.simple_lookup?(markup)
dot_pos = markup.index('.')
if dot_pos.nil?
@name = markup
@lookups = Const::EMPTY_ARRAY
@command_flags = 0
return
end
@name = markup.byteslice(0, dot_pos)
@lookups = []
@command_flags = 0
pos = dot_pos + 1
len = markup.bytesize
while pos < len
seg_start = pos
pos += 1 while pos < len && markup.getbyte(pos) != ByteTables::DOT
seg = markup.byteslice(seg_start, pos - seg_start)
@command_flags |= 1 << @lookups.length if COMMAND_METHODS.include?(seg)
@lookups << seg
pos += 1 # skip dot
end
return
end
lookups = markup.scan(VariableParser)
name = lookups.shift
@@ -114,11 +70,6 @@ module Liquid
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)
# 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] || "")
# 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
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "5.12.0"
VERSION = "5.10.0"
end
-62
View File
@@ -1,62 +0,0 @@
# frozen_string_literal: true
# Quick benchmark: measures parse µs, render µs, and object allocations
# Outputs machine-readable metrics to stdout
require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
runner = ThemeRunner.new
# Warmup — enough iterations for YJIT to fully optimize hot paths
20.times { runner.compile }
20.times { runner.render }
GC.start
GC.compact if GC.respond_to?(:compact)
# Measure parse
parse_times = []
10.times do
GC.disable
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
runner.compile
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
GC.enable
GC.start
parse_times << (t1 - t0) * 1_000_000 # µs
end
# Measure render
render_times = []
10.times do
GC.disable
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
runner.render
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
GC.enable
GC.start
render_times << (t1 - t0) * 1_000_000 # µs
end
# Measure object allocations for one parse+render cycle
require 'objspace'
GC.start
GC.disable
before = ObjectSpace.count_objects.values_at(:TOTAL).first - ObjectSpace.count_objects.values_at(:FREE).first
runner.compile
runner.render
after = ObjectSpace.count_objects.values_at(:TOTAL).first - ObjectSpace.count_objects.values_at(:FREE).first
GC.enable
allocations = after - before
parse_us = parse_times.min.round(0)
render_us = render_times.min.round(0)
combined_us = parse_us + render_us
puts "RESULTS"
puts "parse_us=#{parse_us}"
puts "render_us=#{render_us}"
puts "combined_us=#{combined_us}"
puts "allocations=#{allocations}"
-36
View File
@@ -1,36 +0,0 @@
# 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 |ctx, source, options|
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
# @param ctx [Hash] adapter context containing :template
# @param assigns [Hash] environment variables
# @param options [Hash] :registers, :strict_errors, :exception_renderer
LiquidSpec.render do |ctx, 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]
ctx[:template].render(context)
end
-34
View File
@@ -1,34 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with lax parsing mode
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_lax.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
require 'liquid'
LiquidSpec.configure do |config|
config.features = [:core, :lax_parsing]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
# Force lax mode
options = options.merge(error_mode: :lax)
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
LiquidSpec.render do |ctx, 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]
ctx[:template].render(context)
end
-37
View File
@@ -1,37 +0,0 @@
# 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 plus ActiveSupport SafeBuffer tests
config.features = [:core, :activesupport]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
# @param ctx [Hash] adapter context containing :template
# @param assigns [Hash] environment variables
# @param options [Hash] :registers, :strict_errors, :exception_renderer
LiquidSpec.render do |ctx, 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]
ctx[:template].render(context)
end
-41
View File
@@ -1,41 +0,0 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with YJIT + strict mode + ActiveSupport
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_yjit.rb
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
# Enable YJIT if available
if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
RubyVM::YJIT.enable
end
require 'active_support/all'
require 'liquid'
LiquidSpec.configure do |config|
config.features = [:core, :activesupport]
end
# Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options|
# Force strict mode
options = { error_mode: :strict }.merge(options)
ctx[:template] = Liquid::Template.parse(source, **options)
end
# Render a compiled template with the given context
LiquidSpec.render do |ctx, 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]
ctx[:template].render(context)
end
-15
View File
@@ -639,21 +639,6 @@ class ContextTest < Minitest::Test
end
end
def test_key_lookup_will_raise_for_missing_keys_when_strict_variables_is_enabled
context = Context.new
context.strict_variables = true
assert_raises(Liquid::UndefinedVariable) do
context['unknown']
end
end
def test_has_key_will_not_raise_for_missing_keys_when_strict_variables_is_enabled
context = Context.new
context.strict_variables = true
refute(context.key?('unknown'))
assert_empty(context.errors)
end
def test_context_always_uses_static_registers
registers = {
my_register: :my_value,
+8 -2
View File
@@ -88,11 +88,17 @@ class HashRenderingTest < Minitest::Test
end
def test_rendering_hash_with_custom_to_s_method_uses_custom_to_s
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => HashWithCustomToS.new })
my_hash = Class.new(Hash) do
def to_s
"kewl"
end
end.new
assert_template_result("kewl", "{{ my_hash }}", { "my_hash" => my_hash })
end
def test_rendering_hash_without_custom_to_s_uses_default_inspect
my_hash = HashWithoutCustomToS.new
my_hash = Class.new(Hash).new
my_hash[:foo] = :bar
assert_template_result("{:foo=>:bar}", "{{ my_hash }}", { "my_hash" => my_hash })
+22 -29
View File
@@ -44,40 +44,33 @@ class SecurityTest < Minitest::Test
end
def test_does_not_permanently_add_filters_to_symbol_table
assert_no_new_symbols do
# MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
current_symbols = Symbol.all_symbols
GC.start
end
# MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
GC.start
assert_equal([], (Symbol.all_symbols - current_symbols))
end
def test_does_not_add_drop_methods_to_symbol_table
assert_no_new_symbols do
assigns = { 'drop' => Drop.new }
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
end
end
def assert_no_new_symbols
# Run once to trigger any first-time initialization which might create some symbols,
# for example autoload or lazy method parsing might create symbols on first execution.
yield
# Ensure no new symbols for further runs, i.e. the code does not leak symbols
current_symbols = Symbol.all_symbols
yield
assert_equal([], Symbol.all_symbols - current_symbols)
assigns = { 'drop' => Drop.new }
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
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))
end
def test_max_depth_nested_blocks_does_not_raise_exception
+9 -46
View File
@@ -116,7 +116,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -164,13 +164,6 @@ class StandardFiltersTest < Minitest::Test
assert_equal(['A', 'Z'], @filters.split('A1Z', 1))
end
def test_squish_filter
assert_equal("foo bar boo", Liquid::Template.parse(%({{ " foo bar
\t boo " | squish }})).render)
assert_equal("", Liquid::Template.parse('{{ nil | squish }}').render)
assert_equal("", Liquid::Template.parse('{{ " " | squish }}').render)
end
def test_escape
assert_equal('&lt;strong&gt;', @filters.escape('<strong>'))
assert_equal('1', @filters.escape(1))
@@ -301,7 +294,13 @@ class StandardFiltersTest < Minitest::Test
end
def test_join_calls_to_liquid_on_each_element
assert_equal('i did it, i did it', @filters.join([CustomToLiquidDrop.new('i did it'), CustomToLiquidDrop.new('i did it')], ", "))
drop = Class.new(Liquid::Drop) do
def to_liquid
'i did it'
end
end
assert_equal('i did it, i did it', @filters.join([drop.new, drop.new], ", "))
end
def test_sort
@@ -634,40 +633,6 @@ class StandardFiltersTest < Minitest::Test
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")
#
# Note: ActiveSupport returns "" for empty strings, not nil.
assert_equal('f', @filters.first('foo'))
assert_equal('o', @filters.last('foo'))
assert_equal('', @filters.first(''))
assert_equal('', @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))
@@ -1181,8 +1146,6 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
skip("too slow on non-CRuby due to many exceptions") unless RUBY_ENGINE == 'ruby'
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
test_enum = TestEnumerable.new
@@ -1339,7 +1302,7 @@ class StandardFiltersTest < Minitest::Test
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
+4 -4
View File
@@ -101,7 +101,7 @@ class CycleTagTest < Minitest::Test
assert_template_result("a", template2)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
@@ -129,7 +129,7 @@ class CycleTagTest < Minitest::Test
assert_template_result("N", template5)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
@@ -157,7 +157,7 @@ class CycleTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
@@ -174,7 +174,7 @@ class CycleTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
+5 -5
View File
@@ -204,7 +204,7 @@ class IncludeTagTest < Minitest::Test
)
end
def test_strict2_parsing_errors
def test_rigid_parsing_errors
with_error_modes(:lax, :strict) do
assert_template_result(
'hello value1 value2',
@@ -213,7 +213,7 @@ class IncludeTagTest < Minitest::Test
)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_syntax_error(
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
@@ -408,7 +408,7 @@ class IncludeTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
@@ -421,7 +421,7 @@ class IncludeTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
@@ -434,7 +434,7 @@ class IncludeTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
+11 -8
View File
@@ -101,11 +101,7 @@ class RenderTagTest < Minitest::Test
end
end
def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
end
def test_strict2_parsing_errors
def test_rigid_parsing_errors
with_error_modes(:lax, :strict) do
assert_template_result(
'hello value1 value2',
@@ -114,7 +110,7 @@ class RenderTagTest < Minitest::Test
)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_syntax_error(
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
@@ -294,6 +290,13 @@ class RenderTagTest < Minitest::Test
)
end
def test_render_tag_with_snippet_drop
assert_template_result(
"Hello from snippet",
"{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}",
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
@@ -322,7 +325,7 @@ class RenderTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
@@ -335,7 +338,7 @@ class RenderTagTest < Minitest::Test
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -117,7 +117,7 @@ class StandardTagTest < Minitest::Test
assigns = { 'condition' => "bad string here" }
assert_template_result(
'',
'{% case condition %}{% when "string here" %} hit {% endcase %}',
'{% case condition %}{% when "string here" %} hit {% endcase %}',\
assigns,
)
end
+28 -28
View File
@@ -259,7 +259,7 @@ class TableRowTest < Minitest::Test
)
end
def test_tablerow_with_cols_attribute_in_strict2_mode
def test_tablerow_with_cols_attribute_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
LIQUID
@@ -270,12 +270,12 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">4</td><td class="col2">5</td><td class="col3">6</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_limit_attribute_in_strict2_mode
def test_tablerow_with_limit_attribute_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
LIQUID
@@ -285,12 +285,12 @@ class TableRowTest < Minitest::Test
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_offset_attribute_in_strict2_mode
def test_tablerow_with_offset_attribute_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
LIQUID
@@ -300,12 +300,12 @@ class TableRowTest < Minitest::Test
<td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_range_attribute_in_strict2_mode
def test_tablerow_with_range_attribute_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
LIQUID
@@ -315,12 +315,12 @@ class TableRowTest < Minitest::Test
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_multiple_attributes_in_strict2_mode
def test_tablerow_with_multiple_attributes_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
LIQUID
@@ -331,12 +331,12 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_with_variable_collection_in_strict2_mode
def test_tablerow_with_variable_collection_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -347,12 +347,12 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
end
end
def test_tablerow_with_dotted_access_in_strict2_mode
def test_tablerow_with_dotted_access_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -363,12 +363,12 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
end
end
def test_tablerow_with_bracketed_access_in_strict2_mode
def test_tablerow_with_bracketed_access_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -378,12 +378,12 @@ class TableRowTest < Minitest::Test
<td class="col1">10</td><td class="col2">20</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
end
end
def test_tablerow_without_attributes_in_strict2_mode
def test_tablerow_without_attributes_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
LIQUID
@@ -393,30 +393,30 @@ class TableRowTest < Minitest::Test
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template)
end
end
def test_tablerow_without_in_keyword_in_strict2_mode
def test_tablerow_without_in_keyword_in_rigid_mode
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
end
end
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_strict2_mode
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_rigid_mode
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_equal("Liquid syntax error: Invalid attribute 'invalid1' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid1: 5, invalid2: 10\"", error.message)
end
end
def test_tablerow_with_empty_collection_in_strict2_mode
def test_tablerow_with_empty_collection_in_rigid_mode
template = <<~LIQUID.chomp
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
LIQUID
@@ -426,12 +426,12 @@ class TableRowTest < Minitest::Test
</tr>
OUTPUT
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result(expected, template, { 'empty_array' => [] })
end
end
def test_tablerow_with_invalid_attribute_strict_vs_strict2
def test_tablerow_with_invalid_attribute_strict_vs_rigid
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
expected = <<~OUTPUT
@@ -443,13 +443,13 @@ class TableRowTest < Minitest::Test
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
end
end
def test_tablerow_with_invalid_expression_strict_vs_strict2
def test_tablerow_with_invalid_expression_strict_vs_rigid
template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
with_error_modes(:lax, :strict) do
@@ -460,7 +460,7 @@ class TableRowTest < Minitest::Test
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
+1 -81
View File
@@ -133,7 +133,7 @@ class TemplateTest < Minitest::Test
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
@@ -179,86 +179,6 @@ class TemplateTest < Minitest::Test
assert_equal("すごい", t.render)
end
def test_cumulative_render_score_limit_across_render_tags
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
# Without cumulative limit, all 5 partials render successfully
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
unlimited_output = t.render!
total_cumulative = t.resource_limits.cumulative_render_score
# With cumulative limit set below the total, rendering stops early
t2 = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t2.resource_limits.cumulative_render_score_limit = total_cumulative / 2
limited_output = t2.render
assert(t2.resource_limits.reached?)
assert_operator(limited_output.length, :<, unlimited_output.length)
end
def test_cumulative_render_score_limit_raises_on_render_bang
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t.resource_limits.cumulative_render_score_limit = 20
assert_raises(Liquid::MemoryError) do
t.render!
end
end
def test_cumulative_assign_score_limit_across_include_tags
file_system = StubFileSystem.new(
'assign_partial' => '{% assign x = "a long string value here" %}',
)
environment = Liquid::Environment.build(file_system: file_system)
# Without cumulative limit, all 5 partials render
t = Template.parse(
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
environment: environment,
)
t.render!
total_cumulative = t.resource_limits.cumulative_assign_score
# With cumulative limit set below the total, rendering stops early
t2 = Template.parse(
'{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}{% include "assign_partial" %}',
environment: environment,
)
t2.resource_limits.cumulative_assign_score_limit = total_cumulative / 2
t2.render
assert(t2.resource_limits.reached?)
end
def test_cumulative_render_score_tracks_across_partials_without_limit
file_system = StubFileSystem.new(
'loop' => '{% for a in (1..10) %} foo {% endfor %}',
)
environment = Liquid::Environment.build(file_system: file_system)
t = Template.parse(
'{% render "loop" %}{% render "loop" %}{% render "loop" %}',
environment: environment,
)
t.render!
assert(
t.resource_limits.cumulative_render_score > t.resource_limits.render_score,
"cumulative should exceed per-template score after multiple partials",
)
end
def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new
t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
+5 -5
View File
@@ -218,7 +218,7 @@ class VariableTest < Minitest::Test
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result('helloworld', template)
end
end
@@ -231,7 +231,7 @@ class VariableTest < Minitest::Test
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result('hello12', template)
end
end
@@ -244,7 +244,7 @@ class VariableTest < Minitest::Test
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result('TEST', template)
end
end
@@ -257,7 +257,7 @@ class VariableTest < Minitest::Test
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result('TESTX', template)
end
end
@@ -270,7 +270,7 @@ class VariableTest < Minitest::Test
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
assert_template_result('TESTX', template)
end
end
-20
View File
@@ -199,26 +199,6 @@ class ErrorDrop < Liquid::Drop
end
end
class CustomToLiquidDrop < Liquid::Drop
def initialize(value)
@value = value
super()
end
def to_liquid
@value
end
end
class HashWithCustomToS < Hash
def to_s
"kewl"
end
end
class HashWithoutCustomToS < Hash
end
class StubFileSystem
attr_reader :file_read_count
+7 -173
View File
@@ -161,8 +161,8 @@ class ConditionUnitTest < Minitest::Test
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
@@ -176,19 +176,19 @@ class ConditionUnitTest < Minitest::Test
assert_equal(['title'], result.lookups)
end
def test_parse_expression_in_strict2_mode_raises_internal_error
environment = Environment.build(error_mode: :strict2)
def test_parse_expression_in_rigid_mode_raises_internal_error
environment = Environment.build(error_mode: :rigid)
parse_context = ParseContext.new(environment: environment)
error = assert_raises(Liquid::InternalError) do
Condition.parse_expression(parse_context, 'product.title')
end
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
end
def test_parse_expression_with_safe_true_in_strict2_mode
environment = Environment.build(error_mode: :strict2)
def test_parse_expression_with_safe_true_in_rigid_mode
environment = Environment.build(error_mode: :rigid)
parse_context = ParseContext.new(environment: environment)
result = Condition.parse_expression(parse_context, 'product.title', safe: true)
@@ -197,172 +197,6 @@ class ConditionUnitTest < Minitest::Test
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
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)
-256
View File
@@ -1,256 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
# Tests for the byte-walking fast paths introduced in the ByteTables optimization.
# Each fast path is tested for equivalence with the original regex-based code path
# it replaces, covering accepted inputs, rejected inputs, and boundary cases.
class FastPathUnitTest < Minitest::Test
# ── Expression.parse_number ────────────────────────────────────────
# Fast path: byte-walk with ByteTables::DIGIT
# Replaces: INTEGER_REGEX, FLOAT_REGEX, StringScanner loop
def test_parse_number_simple_integers
assert_equal(42, Liquid::Expression.parse_number("42"))
assert_equal(0, Liquid::Expression.parse_number("0"))
assert_equal(999, Liquid::Expression.parse_number("999"))
end
def test_parse_number_negative_integers
assert_equal(-7, Liquid::Expression.parse_number("-7"))
assert_equal(-0, Liquid::Expression.parse_number("-0"))
assert_equal(-123, Liquid::Expression.parse_number("-123"))
end
def test_parse_number_simple_floats
assert_equal(3.14, Liquid::Expression.parse_number("3.14"))
assert_equal(0.5, Liquid::Expression.parse_number("0.5"))
assert_equal(-0.5, Liquid::Expression.parse_number("-0.5"))
assert_equal(100.0, Liquid::Expression.parse_number("100.0"))
end
def test_parse_number_trailing_dot
# "123." → 123.0 (truncate before dot)
assert_equal(123.0, Liquid::Expression.parse_number("123."))
assert_equal(0.0, Liquid::Expression.parse_number("0."))
end
def test_parse_number_multi_dot_floats
# "1.2.3" → 1.2 (truncate at second dot)
assert_equal(1.2, Liquid::Expression.parse_number("1.2.3"))
assert_equal(1.2, Liquid::Expression.parse_number("1.2.3.4"))
assert_equal(0.0, Liquid::Expression.parse_number("0.0.0"))
end
def test_parse_number_rejects_non_numeric
assert_nil(Liquid::Expression.parse_number("hello"))
assert_nil(Liquid::Expression.parse_number(""))
assert_nil(Liquid::Expression.parse_number("abc123"))
assert_nil(Liquid::Expression.parse_number(".5"))
assert_nil(Liquid::Expression.parse_number("-.5"))
end
def test_parse_number_rejects_trailing_alpha_after_multi_dot
# "1.2.3a" must be nil, not 1.2 — these are not valid numbers
assert_nil(Liquid::Expression.parse_number("1.2.3a"))
assert_nil(Liquid::Expression.parse_number("1.2.3.4a"))
assert_nil(Liquid::Expression.parse_number("1.2.34a"))
assert_nil(Liquid::Expression.parse_number("-1.2.3a"))
end
def test_parse_number_rejects_bare_dash
assert_nil(Liquid::Expression.parse_number("-"))
assert_nil(Liquid::Expression.parse_number("-a"))
end
# ── Expression.parse strip guard ───────────────────────────────────
# Fast path: skip String#strip when no leading/trailing whitespace
# Must produce identical results to unconditional .strip
def test_parse_strips_leading_whitespace
assert_equal(42, Liquid::Expression.parse(" 42"))
assert_equal(42, Liquid::Expression.parse("\t42"))
assert_equal(42, Liquid::Expression.parse("\n42"))
end
def test_parse_strips_trailing_whitespace
assert_equal(42, Liquid::Expression.parse("42 "))
assert_equal(42, Liquid::Expression.parse("42\t"))
assert_equal(42, Liquid::Expression.parse("42\n"))
end
def test_parse_strips_both_sides
assert_equal(42, Liquid::Expression.parse(" 42 "))
assert_equal("hello", Liquid::Expression.parse(" 'hello' "))
end
def test_parse_no_strip_needed
assert_equal(42, Liquid::Expression.parse("42"))
assert_equal("hello", Liquid::Expression.parse("'hello'"))
assert_equal(true, Liquid::Expression.parse("true"))
end
def test_parse_strips_null_bytes
# String#strip removes \x00 — the WHITESPACE table must match
assert_equal(true, Liquid::Expression.parse("\x00true"))
assert_equal(true, Liquid::Expression.parse("true\x00"))
end
# ── VariableLookup.simple_lookup? ──────────────────────────────────
# Fast path: regex gate for simple a.b.c chains
# Must accept only inputs the byte-walk can handle correctly
def test_simple_lookup_accepts_single_names
assert(Liquid::VariableLookup.simple_lookup?("product"))
assert(Liquid::VariableLookup.simple_lookup?("x"))
assert(Liquid::VariableLookup.simple_lookup?("_private"))
end
def test_simple_lookup_accepts_dotted_chains
assert(Liquid::VariableLookup.simple_lookup?("product.title"))
assert(Liquid::VariableLookup.simple_lookup?("a.b.c.d"))
end
def test_simple_lookup_accepts_question_marks
assert(Liquid::VariableLookup.simple_lookup?("product.available?"))
assert(Liquid::VariableLookup.simple_lookup?("empty?"))
end
def test_simple_lookup_accepts_hyphens
assert(Liquid::VariableLookup.simple_lookup?("my-var"))
assert(Liquid::VariableLookup.simple_lookup?("my-var.some-field"))
end
def test_simple_lookup_rejects_brackets
refute(Liquid::VariableLookup.simple_lookup?("product[0]"))
refute(Liquid::VariableLookup.simple_lookup?("hash['key']"))
refute(Liquid::VariableLookup.simple_lookup?("[0]"))
end
def test_simple_lookup_rejects_empty_and_malformed
refute(Liquid::VariableLookup.simple_lookup?(""))
refute(Liquid::VariableLookup.simple_lookup?(".leading"))
refute(Liquid::VariableLookup.simple_lookup?("trailing."))
refute(Liquid::VariableLookup.simple_lookup?("a..b"))
end
# ── VariableLookup fast path equivalence ───────────────────────────
# The fast path must produce identical name, lookups, and command_flags
# to the original VariableParser regex path
def test_fast_path_simple_name
vl = Liquid::VariableLookup.new("product")
assert_equal("product", vl.name)
assert_equal([], vl.lookups)
end
def test_fast_path_dotted_chain
vl = Liquid::VariableLookup.new("product.title")
assert_equal("product", vl.name)
assert_equal(["title"], vl.lookups)
end
def test_fast_path_deep_chain
vl = Liquid::VariableLookup.new("a.b.c.d")
assert_equal("a", vl.name)
assert_equal(["b", "c", "d"], vl.lookups)
end
def test_fast_path_command_methods
vl = Liquid::VariableLookup.new("items.size")
assert_equal("items", vl.name)
assert_equal(["size"], vl.lookups)
assert(vl.lookup_command?(0))
vl2 = Liquid::VariableLookup.new("items.first")
assert(vl2.lookup_command?(0))
vl3 = Liquid::VariableLookup.new("items.last")
assert(vl3.lookup_command?(0))
end
def test_fast_path_non_command_lookups
vl = Liquid::VariableLookup.new("product.title")
refute(vl.lookup_command?(0))
end
def test_fast_path_question_mark
vl = Liquid::VariableLookup.new("product.available?")
assert_equal("product", vl.name)
assert_equal(["available?"], vl.lookups)
end
def test_bracket_lookup_falls_to_regex_path
vl = Liquid::VariableLookup.new("product[0]")
assert_equal("product", vl.name)
assert_equal([0], vl.lookups)
end
# ── BlockBody.try_parse_tag_token ──────────────────────────────────
# Fast path: byte-walk tag tokens instead of FullToken regex
# Must produce identical [tag_name, markup, newline_count] or nil
def test_tag_token_simple
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% if x %}")
assert_equal(["if", "x ", 0], result)
end
def test_tag_token_whitespace_control_leading
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{%- if x %}")
assert_equal(["if", "x ", 0], result)
end
def test_tag_token_whitespace_control_trailing
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% if x -%}")
assert_equal(["if", "x ", 0], result)
end
def test_tag_token_whitespace_control_both
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{%- if x -%}")
assert_equal(["if", "x ", 0], result)
end
def test_tag_token_no_markup
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% endif %}")
assert_equal(["endif", "", 0], result)
end
def test_tag_token_hash_comment
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% # this is a comment %}")
assert_equal(["#", "this is a comment ", 0], result)
end
def test_tag_token_with_newlines
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% \n if \n x %}")
assert_equal(["if", "x ", 2], result)
end
def test_tag_token_hyphenated_name_stops_at_hyphen
# TagName = /\w+/ does not include hyphens
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% my-tag markup %}")
assert_equal("my", result[0])
end
def test_tag_token_complex_markup
body = Liquid::BlockBody.new
result = body.send(:try_parse_tag_token, "{% for item in collection reversed %}")
assert_equal("for", result[0])
assert_equal("item in collection reversed ", result[1])
end
def test_tag_token_malformed_returns_nil
body = Liquid::BlockBody.new
# Token too short
assert_nil(body.send(:try_parse_tag_token, "{%"))
# No valid tag name start (digit)
assert_nil(body.send(:try_parse_tag_token, "{% 123 %}"))
end
end
+27 -27
View File
@@ -9,32 +9,32 @@ class ParseContextUnitTest < Minitest::Test
parser_strict = strict_parse_context.new_parser('product.title')
result_strict = strict_parse_context.safe_parse_expression(parser_strict)
parser_strict2 = strict2_parse_context.new_parser('product.title')
result_strict2 = strict2_parse_context.safe_parse_expression(parser_strict2)
parser_rigid = rigid_parse_context.new_parser('product.title')
result_rigid = rigid_parse_context.safe_parse_expression(parser_rigid)
assert_instance_of(VariableLookup, result_strict)
assert_equal('product', result_strict.name)
assert_equal(['title'], result_strict.lookups)
assert_instance_of(VariableLookup, result_strict2)
assert_equal('product', result_strict2.name)
assert_equal(['title'], result_strict2.lookups)
assert_instance_of(VariableLookup, result_rigid)
assert_equal('product', result_rigid.name)
assert_equal(['title'], result_rigid.lookups)
end
def test_safe_parse_expression_raises_syntax_error_for_invalid_expression
parser_strict = strict_parse_context.new_parser('')
parser_strict2 = strict2_parse_context.new_parser('')
parser_rigid = rigid_parse_context.new_parser('')
error_strict = assert_raises(Liquid::SyntaxError) do
strict_parse_context.safe_parse_expression(parser_strict)
end
assert_match(/is not a valid expression/, error_strict.message)
error_strict2 = assert_raises(Liquid::SyntaxError) do
strict2_parse_context.safe_parse_expression(parser_strict2)
error_rigid = assert_raises(Liquid::SyntaxError) do
rigid_parse_context.safe_parse_expression(parser_rigid)
end
assert_match(/is not a valid expression/, error_strict2.message)
assert_match(/is not a valid expression/, error_rigid.message)
end
def test_parse_expression_with_variable_lookup
@@ -45,10 +45,10 @@ class ParseContextUnitTest < Minitest::Test
assert_equal(['title'], result_strict.lookups)
error = assert_raises(Liquid::InternalError) do
strict2_parse_context.parse_expression('product.title')
rigid_parse_context.parse_expression('product.title')
end
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
end
def test_parse_expression_with_safe_true
@@ -58,11 +58,11 @@ class ParseContextUnitTest < Minitest::Test
assert_equal('product', result_strict.name)
assert_equal(['title'], result_strict.lookups)
result_strict2 = strict2_parse_context.parse_expression('product.title', safe: true)
result_rigid = rigid_parse_context.parse_expression('product.title', safe: true)
assert_instance_of(VariableLookup, result_strict2)
assert_equal('product', result_strict2.name)
assert_equal(['title'], result_strict2.lookups)
assert_instance_of(VariableLookup, result_rigid)
assert_equal('product', result_rigid.name)
assert_equal(['title'], result_rigid.lookups)
end
def test_parse_expression_with_empty_string
@@ -70,40 +70,40 @@ class ParseContextUnitTest < Minitest::Test
assert_nil(result_strict)
error = assert_raises(Liquid::InternalError) do
strict2_parse_context.parse_expression('')
rigid_parse_context.parse_expression('')
end
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
assert_match(/unsafe parse_expression cannot be used in rigid mode/, error.message)
end
def test_parse_expression_with_empty_string_and_safe_true
result_strict = strict_parse_context.parse_expression('', safe: true)
assert_nil(result_strict)
result_strict2 = strict2_parse_context.parse_expression('', safe: true)
assert_nil(result_strict2)
result_rigid = rigid_parse_context.parse_expression('', safe: true)
assert_nil(result_rigid)
end
def test_safe_parse_expression_advances_parser_pointer
parser = strict2_parse_context.new_parser('foo, bar')
parser = rigid_parse_context.new_parser('foo, bar')
# safe_parse_expression consumes "foo"
first_result = strict2_parse_context.safe_parse_expression(parser)
first_result = rigid_parse_context.safe_parse_expression(parser)
assert_instance_of(VariableLookup, first_result)
assert_equal('foo', first_result.name)
parser.consume(:comma)
# safe_parse_expression consumes "bar"
second_result = strict2_parse_context.safe_parse_expression(parser)
second_result = rigid_parse_context.safe_parse_expression(parser)
assert_instance_of(VariableLookup, second_result)
assert_equal('bar', second_result.name)
parser.consume(:end_of_string)
end
def test_parse_expression_with_whitespace_in_strict2_mode
result = strict2_parse_context.parse_expression(' ', safe: true)
def test_parse_expression_with_whitespace_in_rigid_mode
result = rigid_parse_context.parse_expression(' ', safe: true)
assert_nil(result)
end
@@ -115,9 +115,9 @@ class ParseContextUnitTest < Minitest::Test
)
end
def strict2_parse_context
@strict2_parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :strict2),
def rigid_parse_context
@rigid_parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :rigid),
)
end
end
+2 -2
View File
@@ -184,7 +184,7 @@ class PartialCacheUnitTest < Minitest::Test
},
)
[:lax, :warn, :strict, :strict2].each do |error_mode|
[:lax, :warn, :strict, :rigid].each do |error_mode|
Liquid::PartialCache.load(
'my_partial',
context: context,
@@ -193,7 +193,7 @@ class PartialCacheUnitTest < Minitest::Test
end
assert_equal(
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:strict2"],
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:rigid"],
context.registers[:cached_partials].keys,
)
end
-91
View File
@@ -1,91 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class ResourceLimitsUnitTest < Minitest::Test
def test_cumulative_scores_initialize_to_zero
limits = Liquid::ResourceLimits.new({})
assert_equal(0, limits.cumulative_render_score)
assert_equal(0, limits.cumulative_assign_score)
end
def test_cumulative_limits_default_to_nil
limits = Liquid::ResourceLimits.new({})
assert_nil(limits.cumulative_render_score_limit)
assert_nil(limits.cumulative_assign_score_limit)
end
def test_cumulative_limits_configurable_via_hash
limits = Liquid::ResourceLimits.new(
cumulative_render_score_limit: 500,
cumulative_assign_score_limit: 300,
)
assert_equal(500, limits.cumulative_render_score_limit)
assert_equal(300, limits.cumulative_assign_score_limit)
end
def test_cumulative_limits_configurable_via_accessor
limits = Liquid::ResourceLimits.new({})
limits.cumulative_render_score_limit = 500
assert_equal(500, limits.cumulative_render_score_limit)
end
def test_cumulative_scores_survive_reset
limits = Liquid::ResourceLimits.new({})
limits.increment_render_score(10)
limits.increment_assign_score(5)
limits.reset
assert_equal(0, limits.render_score)
assert_equal(0, limits.assign_score)
assert_equal(10, limits.cumulative_render_score)
assert_equal(5, limits.cumulative_assign_score)
end
def test_cumulative_scores_accumulate_across_resets
limits = Liquid::ResourceLimits.new({})
limits.increment_render_score(10)
limits.reset
limits.increment_render_score(20)
limits.reset
limits.increment_render_score(30)
assert_equal(30, limits.render_score)
assert_equal(60, limits.cumulative_render_score)
end
def test_cumulative_render_score_limit_raises
limits = Liquid::ResourceLimits.new(cumulative_render_score_limit: 25)
limits.increment_render_score(10)
limits.reset
limits.increment_render_score(10)
limits.reset
assert_raises(Liquid::MemoryError) do
limits.increment_render_score(10)
end
assert(limits.reached?)
end
def test_cumulative_assign_score_limit_raises
limits = Liquid::ResourceLimits.new(cumulative_assign_score_limit: 15)
limits.increment_assign_score(8)
limits.reset
assert_raises(Liquid::MemoryError) do
limits.increment_assign_score(8)
end
assert(limits.reached?)
end
def test_per_template_limits_still_work_with_cumulative
limits = Liquid::ResourceLimits.new(
render_score_limit: 50,
cumulative_render_score_limit: 1000,
)
assert_raises(Liquid::MemoryError) do
limits.increment_render_score(51)
end
end
end
+1 -1
View File
@@ -8,7 +8,7 @@ class StrainerTemplateUnitTest < Minitest::Test
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = lambda(&:reverse)
wrong_filter = ->(v) { v.reverse }
exception = assert_raises(TypeError) do
s.class.add_filter(wrong_filter)
+6 -26
View File
@@ -24,7 +24,7 @@ class CaseTagUnitTest < Minitest::Test
assert_template_result("one", template)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
@@ -45,7 +45,7 @@ class CaseTagUnitTest < Minitest::Test
assert_template_result("one", template)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
@@ -62,7 +62,7 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict, :strict2) do
with_error_modes(:lax, :strict, :rigid) do
assert_template_result("one", template)
end
end
@@ -77,31 +77,11 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict, :strict2) do
with_error_modes(:lax, :strict, :rigid) do
assert_template_result("one", template)
end
end
def test_case_when_empty
template = <<~LIQUID
{%- case x -%}
{%- when 2 or empty -%}
2 or empty
{%- else -%}
not 2 or empty
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict, :strict2) do
assert_template_result("2 or empty", template, { 'x' => 2 })
assert_template_result("2 or empty", template, { 'x' => {} })
assert_template_result("2 or empty", template, { 'x' => [] })
assert_template_result("not 2 or empty", template, { 'x' => { 'a' => 'b' } })
assert_template_result("not 2 or empty", template, { 'x' => ['a'] })
assert_template_result("not 2 or empty", template, { 'x' => 4 })
end
end
def test_case_with_invalid_expression
template = <<~LIQUID
{%- case foo=>bar -%}
@@ -117,7 +97,7 @@ class CaseTagUnitTest < Minitest::Test
assert_template_result("one", template, assigns)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
@@ -139,7 +119,7 @@ class CaseTagUnitTest < Minitest::Test
assert_template_result("one", template, assigns)
end
with_error_modes(:strict2) do
with_error_modes(:rigid) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
+2 -2
View File
@@ -161,8 +161,8 @@ class VariableUnitTest < Minitest::Test
end
end
def test_strict2_filter_argument_parsing
with_error_modes(:strict2) do
def test_rigid_filter_argument_parsing
with_error_modes(:rigid) do
# optional colon
var = create_variable(%(n | f1 | f2:))
assert_equal([['f1', []], ['f2', []]], var.filters)