Compare commits

...
Author SHA1 Message Date
Charles-P. ClermontandClaude Opus 4.5 6f03452245 style: fix rubocop offenses
Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-01-26 16:52:21 -05:00
Charles-P. ClermontandClaude Opus 4.5 1b420ab6d8 fix(case): use Condition.parse_expression for empty/blank handling
Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-01-26 16:52:21 -05:00
Charles-P. Clermont 18f1e8a61e Replace Context[]'s Expression.parse with Parser#expression 2026-01-26 16:52:17 -05:00
Charles-P. Clermont dff829ec72 Extract Parser#range_lookup out of Expression.parse 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 8b04c52ab2 Extract Parser#variable_lookup out of Expression.parse 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 8efcf7dd3a Move VariableLookup parsing logic to .parse instead of initializer
Goal is to get rid of it entirely, but baby steps.
2026-01-26 16:52:17 -05:00
Charles-P. Clermont af774ebc55 Replace RangeLookup.parse with RangeLookup.create 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 288c4eaac3 Simplify parse_number
We don't need all the multi-dot logic in a world where number comes
out of the Lexer.
2026-01-26 16:52:17 -05:00
Charles-P. Clermont e1e0129983 Extract Parser#number parse out of Expression.parse 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 2a86852eb3 Extract Parser#string out of Expression.parse 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 85d73e462d Remove Expression#safe_parse 2026-01-26 16:52:17 -05:00
Charles-P. Clermont c9ae128354 Rename Parser#expression_node -> Parser#expression 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 6cfcd6cac0 Rename Parser#argument -> argument_string 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 6b78291449 Rename Parser#expression -> Parser#expression_string 2026-01-26 16:52:17 -05:00
Charles-P. Clermont 7d0bdd166d Move parse_expression to Parser.unsafe_parse_expression
- Add Parser#string
- Add Parser#unsafe_parse_expression
- Add private Parser#parse_expression
- Remove ParseContext.parse_expression
- Remove Tag.parse_expression
- Condition.parse_expression now takes a parser as argument
2026-01-26 16:52:17 -05:00
Charles-P. Clermont cf3d845315 Move safe_parse_expression into Parser.expression_node
- Make `Parser` accept the expression cache
- Remove `safe_parse_expression` from `ParseContext`
- Replace all usage of `safe_parse_expression` with `parser.expression_node`
2026-01-26 16:52:17 -05:00
Charles-P. Clermont 851584327a Remove warnings system 2026-01-26 16:52:17 -05:00
Charles-P. ClermontandClaude Opus 4.5 9a3057f8e6 Point liquid-spec to cp-gate-lax-specs-ai branch
This branch gates lax-mode-only specs behind lax_parsing feature:
- shopify_production_recordings: 22 specs
- liquid_ruby: 16 specs
- basics: error regex updated

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-01-26 16:52:06 -05:00
Charles-P. Clermont f4a5454cd6 Remove warnings system 2026-01-14 09:55:21 -05:00
Charles-P. Clermont 40b1f83f25 Update changelog with planned changes for 6.0.0 2026-01-14 09:55:21 -05:00
Charles-P. Clermont 62400f71d0 Remove :error_mode 2026-01-14 09:55:20 -05:00
Charles-P. Clermont b7fa6a89b3 Rename strict2_parse to parse_markup 2026-01-14 09:54:19 -05:00
Charles-P. Clermont d936ce4ac9 Remove :lax, :strict and :warn error modes 2026-01-14 09:54:19 -05:00
49 changed files with 582 additions and 1425 deletions
+2 -1
View File
@@ -32,6 +32,7 @@ group :test do
end
group :spec do
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'main'
# TODO: temporary - using cp-gate-lax-specs-ai branch until lax specs are properly gated
gem 'liquid-spec', github: 'Shopify/liquid-spec', branch: 'cp-gate-lax-specs-ai'
gem 'activesupport', require: false
end
+42
View File
@@ -1,5 +1,47 @@
# Liquid Change Log
## 6.0.0
### Features
* (TODO) Add support for boolean expressions everywhere
* As variable output `{{ a or b }}`
* As filter argument `{{ collection | where: 'prop', a or b }}`
* As tag argument `{% render 'snip', enabled: a or b %}`
* As conditional tag argument `{% if cond %}` (extending previous behaviour)
* (TODO) Add support for subexpression prioritization and associativity
* In ascending order of priority:
* Logical: `and`, `or` (right to left)
* Equality: `==`, `!=`, `<>` (left to right)
* Comparison: `>`, `>=`, `<`, `<=`, `contains` (left to right)
- For example, this is now supported
* `{{ a > b == c < d or e == f }}` which is equivalent to
* `{{ ((a > b) == (c < d)) or (e == f) }}`
- (TODO) Add support for parenthesized expressions
* e.g. `(a or b) and c`
### Architectural changes
* `parse_expression` and `safe_parse_expression` have been removed from `Tag` and `ParseContext`
* `Parser` methods now produce AST nodes instead of strings
* `Parser#expression` produces a value,
* `Parser#string` produces a string,
* etc.
### Breaking changes
* The Environment's `error_mode` option has been removed.
* `:warn` is no longer supported
* `:lax` and `lax_parse` is no longer supported
* `:strict` and `strict_parse` is no longer supported
* `strict2_parse` is renamed to `parse_markup`
* The `warnings` system has been removed.
* `Parser#expression` is renamed to `Parser#expression_string`
* `safe_parse_expression` methods are replaced by `Parser#expression`
* `parse_expression` methods are replaced by `Parser#unsafe_parse_expression`
### Migrating from `^5.11.0`
- In custom tags that include `ParserSwitching`, rename `strict2_parse` to `parse_markup`
- Remove code depending on `:error_mode`
- Replace `safe_parse_expression` calls with `Parser#expression`
## 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]
-25
View File
@@ -93,31 +93,6 @@ LIQUID
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with different parsers that can be used when editing templates to give better error messages
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.
```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby
Liquid::Template.parse(source, error_mode: :strict)
```
This is useful for doing things like enabling strict mode only in the theme editor.
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
+4 -39
View File
@@ -33,29 +33,12 @@ task :rubocop do
end
end
desc('runs test suite with lax, strict, and strict2 parsers')
desc('runs test suite')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict2'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
@@ -78,24 +61,11 @@ task release: :build do
end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :lax do
ruby "./performance/benchmark.rb lax"
desc "Run the liquid benchmark"
task :run do
ruby "./performance/benchmark.rb"
end
desc "Run the liquid benchmark with strict parsing"
task :strict do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with strict2 parsing"
task :strict2 do
ruby "./performance/benchmark.rb strict2"
end
desc "Run the liquid benchmark with lax, strict, and strict2 parsing"
task run: [:lax, :strict, :strict2]
desc "Run unit benchmarks"
namespace :unit do
task :all do
@@ -126,11 +96,6 @@ namespace :profile do
task :run do
ruby "./performance/profile.rb"
end
desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
end
end
namespace :memory_profile do
+3 -2
View File
@@ -48,8 +48,9 @@ module Liquid
@@operators
end
def self.parse_expression(parse_context, markup, safe: false)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
def self.parse_expression(parser)
markup = parser.expression_string
@@method_literals[markup] || parser.unsafe_parse_expression(markup)
end
attr_reader :attachment, :child_condition
+3 -7
View File
@@ -60,10 +60,6 @@ module Liquid
end
# rubocop:enable Metrics/ParameterLists
def warnings
@warnings ||= []
end
def strainer
@strainer ||= @environment.create_strainer(self, @filters)
end
@@ -157,7 +153,6 @@ module Liquid
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
@@ -180,7 +175,8 @@ module Liquid
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(Expression.parse(expression, @string_scanner))
@string_scanner.string = expression
evaluate(Parser.new(@string_scanner).expression)
end
def key?(key)
@@ -244,7 +240,7 @@ module Liquid
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
attr_writer :base_scope_depth, :errors, :strainer, :filters, :disabled_tags
private
+1 -9
View File
@@ -4,10 +4,6 @@ module Liquid
# The Environment is the container for all configuration options of Liquid, such as
# the registered tags, filters, and the default error mode.
class Environment
# The default error mode for all templates. This can be overridden on a
# per-template basis.
attr_accessor :error_mode
# The tags that are available to use in the template.
attr_accessor :tags
@@ -33,17 +29,14 @@ module Liquid
# the template.
# @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).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
# @return [Environment] The new environment instance.
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
def build(tags: nil, file_system: nil, exception_renderer: nil)
ret = new
ret.tags = tags if tags
ret.file_system = file_system if file_system
ret.error_mode = error_mode if error_mode
ret.exception_renderer = exception_renderer if exception_renderer
yield ret if block_given?
ret.freeze
@@ -75,7 +68,6 @@ module Liquid
# @api private
def initialize
@tags = Tags::STANDARD_TAGS.dup
@error_mode = :lax
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
klass.add_filter(StandardFilters)
end
+14 -58
View File
@@ -11,9 +11,6 @@ module Liquid
'false' => false,
'blank' => '',
'empty' => '',
# in lax mode, minus sign can be a VariableLookup
# For simplicity and performace, we treat it like a literal
'-' => VariableLookup.parse("-", nil).freeze,
}.freeze
DOT = ".".ord
@@ -28,10 +25,6 @@ module Liquid
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
parse(parser.expression, ss, cache)
end
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
@@ -56,71 +49,34 @@ module Liquid
def inner_parse(markup, ss, cache)
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Regexp.last_match(2),
ss,
cache,
start_markup = Regexp.last_match(1)
end_markup = Regexp.last_match(2)
start_obj = parse(start_markup, ss, cache)
end_obj = parse(end_markup, ss, cache)
return RangeLookup.create(
start_obj,
end_obj,
start_markup,
end_markup,
)
end
if (num = parse_number(markup, ss))
if (num = parse_number(markup))
num
else
VariableLookup.parse(markup, ss, cache)
end
end
def parse_number(markup, ss)
def parse_number(markup)
# check if the markup is simple integer or float
case markup
when INTEGER_REGEX
return Integer(markup, 10)
Integer(markup, 10)
when FLOAT_REGEX
return markup.to_f
end
ss.string = markup
# the first byte must be a digit or a dash
byte = ss.scan_byte
return false if byte != DASH && (byte < ZERO || byte > NINE)
if byte == DASH
peek_byte = ss.peek_byte
# 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
# 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
end
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
markup.to_f
else
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
false
end
end
end
+3 -25
View File
@@ -3,14 +3,13 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode, :environment
attr_reader :partial, :environment
def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
@locale = @template_options[:locale] ||= I18n.new
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
@@ -38,7 +37,7 @@ module Liquid
def new_parser(input)
@string_scanner.string = input
Parser.new(@string_scanner)
Parser.new(@string_scanner, @expression_cache)
end
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
@@ -50,30 +49,9 @@ module Liquid
)
end
def safe_parse_expression(parser)
Expression.safe_parse(parser, @string_scanner, @expression_cache)
end
def parse_expression(markup, safe: false)
if !safe && @error_mode == :strict2
# parse_expression is a widely used API. To maintain backward
# compatibility while raising awareness about strict2 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
# (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"
end
Expression.parse(markup, @string_scanner, @expression_cache)
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || @environment.error_mode
end
def partial_options
+103 -12
View File
@@ -2,9 +2,10 @@
module Liquid
class Parser
def initialize(input)
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = Lexer.tokenize(ss)
def initialize(input, expression_cache = nil)
@ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@cache = expression_cache
@tokens = Lexer.tokenize(@ss)
@p = 0 # pointer to current location
end
@@ -47,23 +48,75 @@ module Liquid
end
def expression
token = @tokens[@p]
case token[0]
when :id
variable_lookup
when :open_square
unnamed_variable_lookup
when :string
string
when :number
number
when :open_round
range_lookup
else
raise SyntaxError, "#{token} is not a valid expression"
end
end
def number
num = consume(:number)
Expression.parse_number(num)
end
def string
consume(:string)[1..-2]
end
def variable_lookup
name = consume(:id)
lookups, command_flags = variable_lookups
if Expression::LITERALS.key?(name) && lookups.empty?
Expression::LITERALS[name]
else
VariableLookup.new(name, lookups, command_flags)
end
end
def unnamed_variable_lookup
name = indexed_lookup
lookups, command_flags = variable_lookups
VariableLookup.new(name, lookups, command_flags)
end
def range_lookup
consume(:open_round)
first = expression
consume(:dotdot)
last = expression
consume(:close_round)
RangeLookup.create(first, last)
end
def expression_string
token = @tokens[@p]
case token[0]
when :id
str = consume
str << variable_lookups
str << variable_lookups_string
when :open_square
str = consume.dup
str << expression
str << expression_string
str << consume(:close_square)
str << variable_lookups
str << variable_lookups_string
when :string, :number
consume
when :open_round
consume
first = expression
first = expression_string
consume(:dotdot)
last = expression
last = expression_string
consume(:close_round)
"(#{first}..#{last})"
else
@@ -71,23 +124,23 @@ module Liquid
end
end
def argument
def argument_string
str = +""
# might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
str << consume << consume << ' '
end
str << expression
str << expression_string
str
end
def variable_lookups
def variable_lookups_string
str = +""
loop do
if look(:open_square)
str << consume
str << expression
str << expression_string
str << consume(:close_square)
elsif look(:dot)
str << consume
@@ -98,5 +151,43 @@ module Liquid
end
str
end
# Assumes safe input. For cases where you need the string.
# Don't use this unless you're sure about what you're doing.
def unsafe_parse_expression(markup)
parse_expression(markup)
end
private
def parse_expression(markup)
Expression.parse(markup, @ss, @cache)
end
def variable_lookups
lookups = []
command_flags = 0
i = -1
loop do
i += 1
if look(:open_square)
lookups << indexed_lookup
elsif consume?(:dot)
lookup = consume(:id)
lookups << lookup
command_flags |= 1 << i if VariableLookup::COMMAND_METHODS.include?(lookup)
else
break
end
end
[lookups, command_flags]
end
def indexed_lookup
consume(:open_square)
expr = expression
consume(:close_square)
expr
end
end
end
+5 -64
View File
@@ -2,75 +2,16 @@
module Liquid
module ParserSwitching
# Do not use this.
#
# 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.
#
# @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?
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
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end
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 :warn
begin
strict2_parse_with_error_context(markup)
rescue SyntaxError => e
parse_context.warnings << e
lax_parse(markup)
end
end
end
def strict2_mode?
parse_context.error_mode == :strict2 || parse_context.error_mode == :rigid
parse_markup(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
private
def rigid_warn
Deprecations.warn(':rigid', ':strict2')
end
def strict2_parse_with_error_context(markup)
strict2_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end
def markup_context(markup)
"in \"#{markup.strip}\""
end
+1 -1
View File
@@ -4,7 +4,7 @@ module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials]
cache_key = "#{template_name}:#{parse_context.error_mode}"
cache_key = template_name.to_s
cached = cached_partials[cache_key]
return cached if cached
+3 -3
View File
@@ -2,15 +2,15 @@
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup, string_scanner, cache)
def self.create(start_obj, end_obj, start_markup = nil, end_markup = nil)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj)
else
begin
start_obj.to_i..end_obj.to_i
rescue NoMethodError
start_markup = start_obj.to_s unless start_markup
end_markup = end_obj.to_s unless end_markup
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
-10
View File
@@ -65,15 +65,5 @@ module Liquid
def blank?
false
end
private
def safe_parse_expression(parser)
parse_context.safe_parse_expression(parser)
end
def parse_expression(markup, safe: false)
parse_context.parse_expression(markup, safe: safe)
end
end
end
+5 -38
View File
@@ -23,9 +23,6 @@ module Liquid
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
@@ -86,39 +83,23 @@ module Liquid
private
def strict2_parse(markup)
def parse_markup(markup)
parser = @parse_context.new_parser(markup)
@left = safe_parse_expression(parser)
@left = parser.expression
parser.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@left = parse_expression(Regexp.last_match(1))
else
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def record_when_condition(markup)
body = new_body
if strict2_mode?
parse_strict2_when(markup, body)
else
parse_lax_when(markup, body)
end
parse_when(markup, body)
end
def parse_strict2_when(markup, body)
def parse_when(markup, body)
parser = @parse_context.new_parser(markup)
loop do
expr = Condition.parse_expression(parse_context, parser.expression, safe: true)
expr = Condition.parse_expression(parser)
block = Condition.new(@left, '==', expr)
block.attach(body)
@blocks << block
@@ -129,20 +110,6 @@ module Liquid
parser.consume(:end_of_string)
end
def parse_lax_when(markup, body)
while markup
unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
block.attach(body)
@blocks << block
end
end
def record_else_condition(markup)
unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
+4 -35
View File
@@ -15,8 +15,6 @@ module Liquid
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
attr_reader :variables
@@ -56,21 +54,21 @@ module Liquid
private
# cycle [name:] expression(, expression)*
def strict2_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
@variables = []
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
first_expression = safe_parse_expression(p)
first_expression = p.expression
if p.look(:colon)
# cycle name: expr1, expr2, ...
@name = first_expression
@is_named = true
p.consume(:colon)
# After the colon, parse the first variable (required for named cycles)
@variables << maybe_dup_lookup(safe_parse_expression(p))
@variables << maybe_dup_lookup(p.expression)
else
# cycle expr1, expr2, ...
@variables << maybe_dup_lookup(first_expression)
@@ -80,7 +78,7 @@ module Liquid
while p.consume?(:comma)
break if p.look(:end_of_string)
@variables << maybe_dup_lookup(safe_parse_expression(p))
@variables << maybe_dup_lookup(p.expression)
end
p.consume(:end_of_string)
@@ -91,35 +89,6 @@ module Liquid
end
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
case markup
when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
@is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
else
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
next unless Regexp.last_match(1)
var = parse_expression(Regexp.last_match(1))
maybe_dup_lookup(var)
end.compact
end
# For backwards compatibility, whenever a lookup is used in an unnamed cycle,
# we make it so that the @variables.to_s produces different strings for cycles
# called with the same arguments (since @variables.to_s is used as the cycle counter key)
+8 -28
View File
@@ -25,8 +25,6 @@ module Liquid
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
@@ -72,28 +70,13 @@ module Liquid
protected
def lax_parse(markup)
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(collection_name)
markup.scan(TagAttributes) do |key, value|
set_attribute(key, value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.for")
end
end
def strict_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@collection_name = parse_expression(collection_name, safe: true)
collection_name = p.expression_string
@collection_name = p.unsafe_parse_expression(collection_name)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
@@ -104,17 +87,13 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume(:colon)
set_attribute(attribute, p.expression, safe: true)
set_attribute(attribute, p)
end
p.consume(:end_of_string)
end
private
def strict2_parse(markup)
strict_parse(markup)
end
def collection_segment(context)
offsets = context.registers[:for] ||= {}
@@ -178,16 +157,17 @@ module Liquid
output
end
def set_attribute(key, expr, safe: false)
def set_attribute(key, p)
expr = p.expression_string
case key
when 'offset'
@from = if expr == 'continue'
:continue
else
parse_expression(expr, safe: safe)
p.unsafe_parse_expression(expr)
end
when 'limit'
@limit = parse_expression(expr, safe: safe)
@limit = p.unsafe_parse_expression(expr)
end
end
+5 -33
View File
@@ -14,10 +14,6 @@ module Liquid
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render if the condition is met.
class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks
def initialize(tag_name, markup, options)
@@ -66,10 +62,6 @@ module Liquid
private
def strict2_parse(markup)
strict_parse(markup)
end
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
@@ -81,31 +73,11 @@ module Liquid
block.attach(new_body)
end
def parse_expression(markup, safe: false)
Condition.parse_expression(parse_context, markup, safe: safe)
def parse_expression(parser)
Condition.parse_expression(parser)
end
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
until expressions.empty?
operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
condition
end
def strict_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
@@ -124,9 +96,9 @@ module Liquid
end
def parse_comparison(p)
a = parse_expression(p.expression, safe: true)
a = parse_expression(p)
if (op = p.consume?(:comparison))
b = parse_expression(p.expression, safe: true)
b = parse_expression(p)
Condition.new(a, op, b)
else
Condition.new(a)
+4 -30
View File
@@ -20,9 +20,6 @@ module Liquid
class Include < Tag
prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
@@ -84,11 +81,11 @@ module Liquid
alias_method :parse_context, :options
private :parse_context
def strict2_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = safe_parse_expression(p)
@variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with")
@template_name_expr = p.expression
@variable_name_expr = p.expression if p.id?("for") || p.id?("with")
@alias_name = p.consume(:id) if p.id?("as")
p.consume?(:comma)
@@ -97,36 +94,13 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
@attributes[key] = p.expression
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ SYNTAX
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
+6 -29
View File
@@ -27,7 +27,6 @@ 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
disable_tags "include"
@@ -85,12 +84,12 @@ module Liquid
end
# render (string) (with|for expression)? (as id)? (key: value)*
def strict2_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
@template_name_expr = parse_expression(strict2_template_name(p), safe: true)
@template_name_expr = template_name(p)
with_or_for = p.id?("for") || p.id?("with")
@variable_name_expr = safe_parse_expression(p) if with_or_for
@variable_name_expr = p.expression if with_or_for
@alias_name = p.consume(:id) if p.id?("as")
@is_for_loop = (with_or_for == FOR)
@@ -100,37 +99,15 @@ module Liquid
while p.look(:id)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
@attributes[key] = p.expression
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict2_template_name(p)
p.consume(:string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
def template_name(p)
p.string
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
+3 -21
View File
@@ -24,7 +24,6 @@ module Liquid
# @liquid_optional_param offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes
@@ -34,7 +33,7 @@ module Liquid
parse_with_selected_parser(markup)
end
def strict2_parse(markup)
def parse_markup(markup)
p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id)
@@ -43,7 +42,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
end
@collection_name = safe_parse_expression(p)
@collection_name = p.expression
p.consume?(:comma)
@@ -55,30 +54,13 @@ module Liquid
end
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
@attributes[key] = p.expression
p.consume?(:comma)
end
p.consume(:end_of_string)
end
def strict_parse(markup)
lax_parse(markup)
end
def lax_parse(markup)
if markup =~ Syntax
@variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2))
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end
end
def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '')
+1 -16
View File
@@ -16,25 +16,11 @@ module Liquid
#
class Template
attr_accessor :root, :name
attr_reader :resource_limits, :warnings
attr_reader :resource_limits
attr_reader :profiler
class << self
# Sets how strict the parser should be.
# :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
def error_mode=(mode)
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
end
def error_mode
Environment.default.error_mode
end
def default_exception_renderer=(renderer)
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
Environment.default.exception_renderer = renderer
@@ -223,7 +209,6 @@ module Liquid
ParseContext.new(opts)
end
@warnings = parse_context.warnings
parse_context
end
+7 -64
View File
@@ -30,7 +30,7 @@ module Liquid
@parse_context = parse_context
@line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup)
parse_with_selected_parser(markup)
end
def raw
@@ -41,58 +41,17 @@ module Liquid
"in \"{{#{markup}}}\""
end
def lax_parse(markup)
@filters = []
return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2)
@name = parse_context.parse_expression(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
@filters << lax_parse_filter_expressions(filtername, filterargs)
end
end
end
def strict_parse(markup)
def parse_markup(markup)
@filters = []
p = @parse_context.new_parser(markup)
return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p)
while p.consume?(:pipe)
filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
@filters << lax_parse_filter_expressions(filtername, filterargs)
end
@name = p.expression
@filters << parse_filter_expressions(p) while p.consume?(:pipe)
p.consume(:end_of_string)
end
def strict2_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)
p.consume(:end_of_string)
end
def parse_filterargs(p)
# first argument
filterargs = [p.argument]
# followed by comma separated others
filterargs << p.argument while p.consume?(:comma)
filterargs
end
def render(context)
obj = context.evaluate(@name)
@@ -133,22 +92,6 @@ module Liquid
private
def lax_parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
else
filter_args << parse_context.parse_expression(a)
end
end
result = [filter_name, filter_args]
result << keyword_args if keyword_args
result
end
# Surprisingly, positional and keyword arguments can be mixed.
#
# filter = filtername [":" filterargs?]
@@ -156,7 +99,7 @@ module Liquid
# argument = (positional_argument | keyword_argument)
# positional_argument = expression
# keyword_argument = id ":" expression
def strict2_parse_filter_expressions(p)
def parse_filter_expressions(p)
filtername = p.consume(:id)
filter_args = []
keyword_args = {}
@@ -178,10 +121,10 @@ module Liquid
if p.look(:id) && p.look(:colon, 1)
key = p.consume(:id)
p.consume(:colon)
value = parse_context.safe_parse_expression(p)
value = p.expression
keyword_arguments[key] = value
else
positional_arguments << parse_context.safe_parse_expression(p)
positional_arguments << p.expression
end
end
+11 -9
View File
@@ -7,10 +7,6 @@ module Liquid
attr_reader :name, :lookups
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup, string_scanner, cache)
end
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
lookups = markup.scan(VariableParser)
name = lookups.shift
@@ -21,12 +17,10 @@ module Liquid
cache,
)
end
@name = name
@lookups = lookups
@command_flags = 0
command_flags = 0
@lookups.each_index do |i|
lookups.each_index do |i|
lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(
@@ -35,9 +29,17 @@ module Liquid
cache,
)
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
command_flags |= 1 << i
end
end
new(name, lookups, command_flags)
end
def initialize(name, lookups, command_flags)
@name = name
@lookups = lookups
@command_flags = command_flags
end
def lookup_command?(lookup_index)
-1
View File
@@ -4,7 +4,6 @@ require 'benchmark/ips'
require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
-2
View File
@@ -53,8 +53,6 @@ class Profiler
end
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
runner = ThemeRunner.new
Profiler.run do |x|
x.profile('parse') { runner.compile }
-1
View File
@@ -3,7 +3,6 @@
require 'stackprof'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.run
+32 -21
View File
@@ -6,7 +6,7 @@ require "benchmark/ips"
require 'liquid'
RubyVM::YJIT.enable
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
STRING_MARKUPS = [
"\"foo\"",
@@ -45,22 +45,14 @@ NUMBER_MARKUPS = [
RANGE_MARKUPS = [
"(1..30)",
"(1...30)",
"(1..30..5)",
"(1.0...30.0)",
"(1.........30)",
"(1..foo)",
"(foo..30)",
"(foo..bar)",
"(foo...bar...100)",
"(foo...bar...100.0)",
]
LITERAL_MARKUPS = [
nil,
'nil',
'null',
'',
'true',
'false',
'blank',
@@ -75,20 +67,39 @@ MARKUPS = {
"range" => RANGE_MARKUPS,
}
Benchmark.ips do |x|
x.config(time: 5, warmup: 5)
module Liquid
Benchmark.ips do |x|
x.config(time: 5, warmup: 5)
MARKUPS.each do |type, markups|
x.report("Liquid::Expression#parse: #{type}") do
markups.each do |markup|
Liquid::Expression.parse(markup)
ss = StringScanner.new('')
MARKUPS.each do |type, markups|
x.report("#{type} - Liquid::Expression#parse") do
markups.each do |markup|
ss.string = markup
Expression.parse(markup, ss)
end
end
x.report("#{type} - Liquid::Parser#expression") do
markups.each do |markup|
ss.string = markup
Parser.new(ss).expression
end
end
x.report("#{type} - Liquid::Expression.parse(Parser#expression_string)") do
markups.each do |markup|
ss.string = markup
Expression.parse(Parser.new(ss).expression_string, ss)
end
end
end
x.report("Liquid::Expression#parse: all") do
MARKUPS.values.flatten.each do |markup|
Expression.parse(markup)
end
end
end
x.report("Liquid::Expression#parse: all") do
MARKUPS.values.flatten.each do |markup|
Liquid::Expression.parse(markup)
end
end
end
+4 -3
View File
@@ -6,7 +6,7 @@ require "benchmark/ips"
require 'liquid'
RubyVM::YJIT.enable
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
EXPRESSIONS = [
"foo[1..2].baz",
@@ -31,11 +31,12 @@ EXPRESSIONS = [
Benchmark.ips do |x|
x.config(time: 10, warmup: 5)
ss = StringScanner.new('')
x.report("Liquid::Lexer#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer.new(expr)
l.tokenize
ss.string = expr
Liquid::Lexer.tokenize(ss)
end
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
+1 -3
View File
@@ -1,6 +1,6 @@
# frozen_string_literal: true
# Liquid Spec Adapter for Shopify/liquid with YJIT + strict mode + ActiveSupport
# Liquid Spec Adapter for Shopify/liquid with YJIT + ActiveSupport
#
# Run with: bundle exec liquid-spec run spec/ruby_liquid_yjit.rb
@@ -20,8 +20,6 @@ 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
+2 -4
View File
@@ -39,13 +39,11 @@ class AssignTest < Minitest::Test
assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
end
def test_assign_uses_error_mode
def test_assign_throws_on_unsupported_syntax
assert_match_syntax_error(
"Expected dotdot but found pipe in ",
"Expected dotdot but found pipe",
"{% assign foo = ('X' | downcase) %}",
error_mode: :strict,
)
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
end
def test_expression_with_whitespace_in_square_brackets
+3 -5
View File
@@ -632,11 +632,9 @@ class ContextTest < Minitest::Test
end
def test_has_key_will_not_add_an_error_for_missing_keys
with_error_modes(:strict) do
context = Context.new
context.key?('unknown')
assert_empty(context.errors)
end
context = Context.new
context.key?('unknown')
assert_empty(context.errors)
end
def test_key_lookup_will_raise_for_missing_keys_when_strict_variables_is_enabled
+4 -51
View File
@@ -67,20 +67,11 @@ class ErrorHandlingTest < Minitest::Test
end
def test_unrecognized_operator
with_error_modes(:strict) do
assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end
def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal(' Liquid error: Unknown operator =! ', template.render)
assert_equal(1, template.errors.size)
assert_equal(Liquid::ArgumentError, template.errors.first.class)
end
def test_with_line_numbers_adds_numbers_to_parser_errors
source = <<~LIQUID
foobar
@@ -104,25 +95,6 @@ class ErrorHandlingTest < Minitest::Test
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse(
'
foobar
{% if 1 =! 2 %}ok{% endif %}
bla
',
error_mode: :warn,
line_numbers: true,
)
assert_equal(
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message),
)
end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do
Liquid::Template.parse(
@@ -133,7 +105,6 @@ class ErrorHandlingTest < Minitest::Test
bla
',
error_mode: :strict,
line_numbers: true,
)
end
@@ -157,34 +128,16 @@ class ErrorHandlingTest < Minitest::Test
def test_strict_error_messages
err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
Liquid::Template.parse('{{%%%}}')
end
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
end
def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
assert_equal(3, template.warnings.size)
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
assert_equal('', template.render)
end
def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
assert_equal(3, template.warnings.size)
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raises(Exception) do
+10 -28
View File
@@ -27,11 +27,6 @@ class ExpressionTest < Minitest::Test
assert_template_result("-17.42", "{{ -17.42 }}")
assert_template_result("2.5", "{{ 2.5 }}")
with_error_modes(:lax) do
assert_expression_result(0.0, "0.....5")
assert_expression_result(0.0, "-0..1")
end
assert_expression_result(1.5, "1.5")
# this is a unfortunate quirky behavior of Liquid
@@ -51,24 +46,11 @@ class ExpressionTest < Minitest::Test
"{{ (false..true) }}",
)
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"Liquid syntax error (line 1): Invalid expression type '1..2' in range expression",
"{{ ((1..2)..3) }}",
)
end
def test_quirky_negative_sign_expression_markup
result = Expression.parse("-", nil)
assert(result.is_a?(VariableLookup))
assert_equal("-", result.name)
# for this template, the expression markup is "-"
assert_template_result(
"",
"{{ - 'theme.css' - }}",
error_mode: :lax,
)
end
def test_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
@@ -85,7 +67,7 @@ class ExpressionTest < Minitest::Test
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
[],
cache.to_a.map { _1[0] }.sort,
)
end
@@ -109,7 +91,7 @@ class ExpressionTest < Minitest::Test
cache = parse_context.instance_variable_get(:@expression_cache)
assert_equal(
["1", "2", "x", "y"],
[],
cache.to_a.map { _1[0] }.sort,
)
end
@@ -130,7 +112,7 @@ class ExpressionTest < Minitest::Test
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
[],
cache.to_a.map { _1[0] }.sort,
)
end
@@ -152,30 +134,30 @@ class ExpressionTest < Minitest::Test
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
end
def test_safe_parse_with_variable_lookup
def test_parser_expression_with_variable_lookup
parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('product.title')
result = Liquid::Expression.safe_parse(parser)
result = parser.expression
assert_instance_of(Liquid::VariableLookup, result)
assert_equal('product', result.name)
assert_equal(['title'], result.lookups)
end
def test_safe_parse_with_number
def test_parser_expression_with_number
parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('42')
result = Liquid::Expression.safe_parse(parser)
result = parser.expression
assert_equal(42, result)
end
def test_safe_parse_raises_syntax_error_for_invalid_expression
def test_parser_expression_raises_syntax_error_for_invalid_expression
parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('')
error = assert_raises(Liquid::SyntaxError) do
Liquid::Expression.safe_parse(parser)
parser.expression
end
assert_match(/is not a valid expression/, error.message)
+10 -89
View File
@@ -31,58 +31,25 @@ class ParsingQuirksTest < Minitest::Test
def test_error_on_empty_filter
assert(Template.parse("{{test}}"))
with_error_modes(:lax) do
assert(Template.parse("{{|test}}"))
end
with_error_modes(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end
assert_raises(Liquid::SyntaxError) { Template.parse("{{|test}}") }
assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") }
end
def test_meaningless_parens_error
with_error_modes(:strict) do
assert_raises(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raises(SyntaxError) do
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end
def test_unexpected_characters_syntax_error
with_error_modes(:strict) do
assert_raises(SyntaxError) do
markup = "true && false"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raises(SyntaxError) do
markup = "false || true"
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end
end
def test_no_error_on_lax_empty_filter
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
assert(Template.parse("{{test}}", error_mode: :lax))
assert(Template.parse("{{|test|}}", error_mode: :lax))
end
def test_meaningless_parens_lax
with_error_modes(:lax) do
assigns = { 'b' => 'bar', 'c' => 'baz' }
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
end
end
def test_unexpected_characters_silently_eat_logic_lax
with_error_modes(:lax) do
assert_raises(SyntaxError) do
markup = "true && false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}")
Template.parse("{% if #{markup} %} YES {% endif %}")
end
assert_raises(SyntaxError) do
markup = "false || true"
assert_template_result('', "{% if #{markup} %} YES {% endif %}")
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end
@@ -92,32 +59,6 @@ class ParsingQuirksTest < Minitest::Test
end
end
def test_unanchored_filter_arguments
with_error_modes(:lax) do
assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}")
assert_template_result('x', "{{ 'X' | downcase) }}")
# After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end
end
def test_invalid_variables_work
with_error_modes(:lax) do
assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}")
assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}")
end
end
def test_extra_dots_in_ranges
with_error_modes(:lax) do
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end
end
def test_blank_variable_markup
assert_template_result('', "{{}}")
end
@@ -131,24 +72,4 @@ class ParsingQuirksTest < Minitest::Test
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end
def test_incomplete_expression
with_error_modes(:lax) do
assert_template_result("false", "{{ false - }}")
assert_template_result("false", "{{ false > }}")
assert_template_result("false", "{{ false < }}")
assert_template_result("false", "{{ false = }}")
assert_template_result("false", "{{ false ! }}")
assert_template_result("false", "{{ false 1 }}")
assert_template_result("false", "{{ false a }}")
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
end
end
end # ParsingQuirksTest
+22 -51
View File
@@ -91,28 +91,21 @@ class CycleTagTest < Minitest::Test
assert_match(/Syntax Error in 'cycle' - Valid syntax: cycle \[name :\] var/, error.message)
end
def test_cycle_tag_with_error_mode
def test_cycle_tag_unsupported_legacy_quirk
# QuotedFragment is more permissive than what Parser#expression allows.
template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
template2 = "{% cycle .5: 'a', 'b' %}"
with_error_modes(:lax, :strict) do
assert_template_result("b", template1)
assert_template_result("a", template2)
end
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
with_error_modes(:strict2) do
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
end
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
end
def test_cycle_with_trailing_elements
def test_cycle_with_trailing_elements_legacy_syntax
assignments = "{% assign a = 'A' %}{% assign n = 'N' %}"
template1 = "#{assignments}{% cycle 'a' 'b', 'c' %}"
@@ -121,29 +114,19 @@ class CycleTagTest < Minitest::Test
template4 = "#{assignments}{% cycle n e: 'a', 'b', 'c' %}"
template5 = "#{assignments}{% cycle n e 'a', 'b', 'c' %}"
with_error_modes(:lax, :strict) do
assert_template_result("a", template1)
assert_template_result("a", template2)
assert_template_result("a", template3)
assert_template_result("N", template4)
assert_template_result("N", template5)
end
error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
with_error_modes(:strict2) 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) }
error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
expected_error = /Expected end_of_string but found/
expected_error = /Expected end_of_string but found/
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
assert_match(expected_error, error3.message)
assert_match(expected_error, error4.message)
assert_match(expected_error, error5.message)
end
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
assert_match(expected_error, error3.message)
assert_match(expected_error, error4.message)
assert_match(expected_error, error5.message)
end
def test_cycle_name_with_invalid_expression
@@ -153,14 +136,8 @@ class CycleTagTest < Minitest::Test
{% endfor %}
LIQUID
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
def test_cycle_variable_with_invalid_expression
@@ -170,13 +147,7 @@ class CycleTagTest < Minitest::Test
{% endfor %}
LIQUID
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end
+16 -50
View File
@@ -204,23 +204,13 @@ class IncludeTagTest < Minitest::Test
)
end
def test_strict2_parsing_errors
with_error_modes(:lax, :strict) do
assert_template_result(
'hello value1 value2',
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
)
end
with_error_modes(:strict2) do
assert_syntax_error(
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% include "snippet" | filter %}',
)
end
def test_parsing_errors_for_legacy_quirk
assert_syntax_error(
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% include "snippet" | filter %}',
)
end
def test_optional_commas
@@ -301,16 +291,10 @@ class IncludeTagTest < Minitest::Test
env = Liquid::Environment.build(file_system: TestFileSystem.new)
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
end
with_error_modes(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
Template.parse("{% include template %}", environment: env).render!("template" => '{{ "X" || downcase }}')
end
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
end
with_error_modes(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
Template.parse("{% include template %}", include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
end
end
@@ -365,7 +349,7 @@ class IncludeTagTest < Minitest::Test
file_system: StubFileSystem.new('simple' => 'simple'),
)
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
template = Liquid::Template.parse("{% include 'simple' %}", environment: env)
template.render(nil, strict_variables: true)
assert_equal([], template.errors)
@@ -404,39 +388,21 @@ class IncludeTagTest < Minitest::Test
def test_include_template_with_invalid_expression
template = "{% include foo=>bar %}"
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
def test_include_with_invalid_expression
template = '{% include "snippet" with foo=>bar %}'
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
def test_include_attribute_with_invalid_expression
template = '{% include "snippet", key: foo=>bar %}'
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end # IncludeTagTest
+11 -35
View File
@@ -105,23 +105,13 @@ class RenderTagTest < Minitest::Test
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
end
def test_strict2_parsing_errors
with_error_modes(:lax, :strict) do
assert_template_result(
'hello value1 value2',
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' },
)
end
with_error_modes(:strict2) do
assert_syntax_error(
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% render "snippet" | filter %}',
)
end
def test_parsing_errors_legacy_syntax
assert_syntax_error(
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% render "snippet" | filter %}',
)
end
def test_optional_commas
@@ -317,27 +307,13 @@ class RenderTagTest < Minitest::Test
def test_render_with_invalid_expression
template = '{% render "snippet" with foo=>bar %}'
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
def test_render_attribute_with_invalid_expression
template = '{% render "snippet", key: foo=>bar %}'
with_error_modes(:lax, :strict) do
refute_nil(Template.parse(template))
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end
+32 -102
View File
@@ -188,29 +188,6 @@ class TableRowTest < Minitest::Test
assert_template_result(expected_output, template)
end
def test_table_row_renders_correct_error_message_for_invalid_parameters
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
error_mode: :warn,
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
error_mode: :warn,
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
error_mode: :warn,
)
end
def test_table_row_handles_interrupts
assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
@@ -259,7 +236,7 @@ class TableRowTest < Minitest::Test
)
end
def test_tablerow_with_cols_attribute_in_strict2_mode
def test_tablerow_with_cols_attribute
template = <<~LIQUID.chomp
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
LIQUID
@@ -270,12 +247,10 @@ 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
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_with_limit_attribute_in_strict2_mode
def test_tablerow_with_limit_attribute
template = <<~LIQUID.chomp
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
LIQUID
@@ -285,12 +260,10 @@ 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
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_with_offset_attribute_in_strict2_mode
def test_tablerow_with_offset_attribute
template = <<~LIQUID.chomp
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
LIQUID
@@ -300,12 +273,10 @@ 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
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_with_range_attribute_in_strict2_mode
def test_tablerow_with_range_attribute
template = <<~LIQUID.chomp
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
LIQUID
@@ -315,12 +286,10 @@ 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
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_with_multiple_attributes_in_strict2_mode
def test_tablerow_with_multiple_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
LIQUID
@@ -331,12 +300,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_with_variable_collection_in_strict2_mode
def test_tablerow_with_variable_collection
template = <<~LIQUID.chomp
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -347,12 +314,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
end
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
end
def test_tablerow_with_dotted_access_in_strict2_mode
def test_tablerow_with_dotted_access
template = <<~LIQUID.chomp
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -363,12 +328,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
end
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
end
def test_tablerow_with_bracketed_access_in_strict2_mode
def test_tablerow_with_bracketed_access
template = <<~LIQUID.chomp
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
LIQUID
@@ -378,12 +341,10 @@ class TableRowTest < Minitest::Test
<td class="col1">10</td><td class="col2">20</td></tr>
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
end
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
end
def test_tablerow_without_attributes_in_strict2_mode
def test_tablerow_without_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
LIQUID
@@ -393,30 +354,24 @@ 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
assert_template_result(expected, template)
end
assert_template_result(expected, template)
end
def test_tablerow_without_in_keyword_in_strict2_mode
def test_tablerow_without_in_keyword
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) 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
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
def test_tablerow_with_multiple_invalid_attributes_reports_first_in_strict2_mode
def test_tablerow_with_multiple_invalid_attributes_reports_first
template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) 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
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
def test_tablerow_with_empty_collection_in_strict2_mode
def test_tablerow_with_empty_collection
template = <<~LIQUID.chomp
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
LIQUID
@@ -426,43 +381,18 @@ class TableRowTest < Minitest::Test
</tr>
OUTPUT
with_error_modes(:strict2) do
assert_template_result(expected, template, { 'empty_array' => [] })
end
assert_template_result(expected, template, { 'empty_array' => [] })
end
def test_tablerow_with_invalid_attribute_strict_vs_strict2
def test_tablerow_with_invalid_attribute
template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
expected = <<~OUTPUT
<tr class="row1">
<td class="col1">1</td><td class="col2">2</td><td class="col3">3</td><td class="col4">4</td><td class="col5">5</td></tr>
OUTPUT
with_error_modes(:lax, :strict) do
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
end
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Invalid attribute 'invalid_attr'/, error.message)
end
def test_tablerow_with_invalid_expression_strict_vs_strict2
def test_tablerow_with_invalid_expression
template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
with_error_modes(:lax, :strict) do
expected = <<~OUTPUT
<tr class="row1">
</tr>
OUTPUT
assert_template_result(expected, template)
end
with_error_modes(:strict2) do
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
error = assert_raises(SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end
+1 -11
View File
@@ -44,16 +44,6 @@ class TemplateTest < Minitest::Test
assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end
def test_warnings_is_not_exponential_time
str = "false"
100.times do
str = "{% if true %}true{% else %}#{str}{% endif %}"
end
t = Template.parse(str)
assert_equal([], Timeout.timeout(1) { t.warnings })
end
def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal('foo', t.render!)
@@ -259,7 +249,7 @@ class TemplateTest < Minitest::Test
end
def test_nil_value_does_not_raise
t = Template.parse("some{{x}}thing", error_mode: :strict)
t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count)
+5 -74
View File
@@ -176,102 +176,33 @@ class VariableTest < Minitest::Test
)
end
def test_variable_lookup_should_not_hang_with_invalid_syntax
Timeout.timeout(1) do
assert_template_result(
'bar',
"{{['foo'}}",
{
'foo' => 'bar',
},
error_mode: :lax,
)
end
very_long_key = "1234567890" * 100
template_list = [
"{{['#{very_long_key}']}}", # valid
"{{['#{very_long_key}'}}", # missing closing bracket
"{{[['#{very_long_key}']}}", # extra open bracket
]
template_list.each do |template|
Timeout.timeout(1) do
assert_template_result(
'bar',
template,
{
very_long_key => 'bar',
},
error_mode: :lax,
)
end
end
end
def test_filter_with_single_trailing_comma
template = '{{ "hello" | append: "world", }}'
with_error_modes(:strict) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
assert_template_result('helloworld', template)
end
assert_template_result('helloworld', template)
end
def test_multiple_filters_with_trailing_commas
template = '{{ "hello" | append: "1", | append: "2", }}'
with_error_modes(:strict) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
assert_template_result('hello12', template)
end
assert_template_result('hello12', template)
end
def test_filter_with_colon_but_no_arguments
template = '{{ "test" | upcase: }}'
with_error_modes(:strict) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
assert_template_result('TEST', template)
end
assert_template_result('TEST', template)
end
def test_filter_chain_with_colon_no_args
template = '{{ "test" | append: "x" | upcase: }}'
with_error_modes(:strict) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
assert_template_result('TESTX', template)
end
assert_template_result('TESTX', template)
end
def test_combining_trailing_comma_and_empty_args
template = '{{ "test" | append: "x", | upcase: }}'
with_error_modes(:strict) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/is not a valid expression/, error.message)
end
with_error_modes(:strict2) do
assert_template_result('TESTX', template)
end
assert_template_result('TESTX', template)
end
end
+6 -23
View File
@@ -8,13 +8,6 @@ $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib'))
require 'liquid.rb'
require 'liquid/profiler'
mode = :strict
if (env_mode = ENV['LIQUID_PARSER_MODE'])
puts "-- #{env_mode.upcase} ERROR MODE"
mode = env_mode.to_sym
end
Liquid::Environment.default.error_mode = mode
if Minitest.const_defined?('Test')
# We're on Minitest 5+. Nothing to do here.
else
@@ -34,27 +27,27 @@ module Minitest
def assert_template_result(
expected, template, assigns = {},
message: nil, partials: nil, error_mode: Liquid::Environment.default.error_mode, render_errors: false,
message: nil, partials: nil, render_errors: false,
template_factory: nil
)
file_system = StubFileSystem.new(partials || {})
environment = Liquid::Environment.build(file_system: file_system)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
template = Liquid::Template.parse(template, line_numbers: true, environment: environment)
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
output = template.render(context)
assert_equal(expected, output, message)
end
def assert_match_syntax_error(match, template, error_mode: nil)
def assert_match_syntax_error(match, template)
exception = assert_raises(Liquid::SyntaxError) do
Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym).render
Template.parse(template, line_numbers: true).render
end
assert_match(match, exception.message)
end
def assert_syntax_error(template, error_mode: nil)
assert_match_syntax_error("", template, error_mode: error_mode)
def assert_syntax_error(template)
assert_match_syntax_error("", template)
end
def assert_usage_increment(name, times: 1)
@@ -82,16 +75,6 @@ module Minitest
Environment.dangerously_override(environment, &blk)
end
def with_error_modes(*modes)
old_mode = Liquid::Environment.default.error_mode
modes.each do |mode|
Liquid::Environment.default.error_mode = mode
yield
end
ensure
Liquid::Environment.default.error_mode = old_mode
end
def with_custom_tag(tag_name, tag_class, &block)
environment = Liquid::Environment.default.dup
environment.register_tag(tag_name, tag_class)
+29 -40
View File
@@ -82,7 +82,7 @@ class ConditionUnitTest < Minitest::Test
def test_contains_works_on_arrays
@context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array")
array_expr = VariableLookup.parse("array")
assert_evaluates_false(array_expr, 'contains', 0)
assert_evaluates_true(array_expr, 'contains', 1)
@@ -96,8 +96,8 @@ class ConditionUnitTest < Minitest::Test
def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new
assert_evaluates_false(VariableLookup.new('not_assigned'), 'contains', '0')
assert_evaluates_false(0, 'contains', VariableLookup.new('not_assigned'))
assert_evaluates_false(VariableLookup.parse('not_assigned'), 'contains', '0')
assert_evaluates_false(0, 'contains', VariableLookup.parse('not_assigned'))
end
def test_contains_return_false_on_wrong_data_type
@@ -149,7 +149,7 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
assert_evaluates_true(VariableLookup.parse("one"), '==', VariableLookup.parse("another"))
end
def test_default_context_is_deprecated
@@ -166,35 +166,24 @@ class ConditionUnitTest < Minitest::Test
assert_includes(err.lines.map(&:strip), expected)
end
def test_parse_expression_in_strict_mode
environment = Environment.build(error_mode: :strict)
def test_parse_expression
environment = Environment.build
parse_context = ParseContext.new(environment: environment)
result = Condition.parse_expression(parse_context, 'product.title')
parser = parse_context.new_parser('product.title')
result = Condition.parse_expression(parser)
assert_instance_of(VariableLookup, result)
assert_equal('product', result.name)
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_returns_method_literal_for_blank_and_empty
environment = Environment.build
parse_context = ParseContext.new(environment: environment)
parser = parse_context.new_parser('blank')
result = Condition.parse_expression(parser)
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)
end
def test_parse_expression_with_safe_true_in_strict2_mode
environment = Environment.build(error_mode: :strict2)
parse_context = ParseContext.new(environment: environment)
result = Condition.parse_expression(parse_context, 'product.title', safe: true)
assert_instance_of(VariableLookup, result)
assert_equal('product', result.name)
assert_equal(['title'], result.lookups)
assert_instance_of(Condition::MethodLiteral, result)
end
# Tests for blank? comparison without ActiveSupport
@@ -216,7 +205,7 @@ class ConditionUnitTest < Minitest::Test
@context['whitespace'] = ' '
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('whitespace'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('whitespace'), '==', blank_literal)
end
def test_blank_with_empty_string
@@ -225,7 +214,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_string'] = ''
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('empty_string'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', blank_literal)
end
def test_blank_with_empty_array
@@ -234,7 +223,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_array'] = []
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('empty_array'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', blank_literal)
end
def test_blank_with_empty_hash
@@ -243,7 +232,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_hash'] = {}
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', blank_literal)
end
def test_blank_with_nil
@@ -252,7 +241,7 @@ class ConditionUnitTest < Minitest::Test
@context['nil_value'] = nil
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('nil_value'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('nil_value'), '==', blank_literal)
end
def test_blank_with_false
@@ -261,7 +250,7 @@ class ConditionUnitTest < Minitest::Test
@context['false_value'] = false
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_true(VariableLookup.new('false_value'), '==', blank_literal)
assert_evaluates_true(VariableLookup.parse('false_value'), '==', blank_literal)
end
def test_not_blank_with_true
@@ -270,7 +259,7 @@ class ConditionUnitTest < Minitest::Test
@context['true_value'] = true
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_false(VariableLookup.new('true_value'), '==', blank_literal)
assert_evaluates_false(VariableLookup.parse('true_value'), '==', blank_literal)
end
def test_not_blank_with_number
@@ -279,7 +268,7 @@ class ConditionUnitTest < Minitest::Test
@context['number'] = 42
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_false(VariableLookup.new('number'), '==', blank_literal)
assert_evaluates_false(VariableLookup.parse('number'), '==', blank_literal)
end
def test_not_blank_with_string_content
@@ -288,7 +277,7 @@ class ConditionUnitTest < Minitest::Test
@context['string'] = 'hello'
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_false(VariableLookup.new('string'), '==', blank_literal)
assert_evaluates_false(VariableLookup.parse('string'), '==', blank_literal)
end
def test_not_blank_with_non_empty_array
@@ -297,7 +286,7 @@ class ConditionUnitTest < Minitest::Test
@context['array'] = [1, 2, 3]
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_false(VariableLookup.new('array'), '==', blank_literal)
assert_evaluates_false(VariableLookup.parse('array'), '==', blank_literal)
end
def test_not_blank_with_non_empty_hash
@@ -306,7 +295,7 @@ class ConditionUnitTest < Minitest::Test
@context['hash'] = { 'a' => 1 }
blank_literal = Condition.class_variable_get(:@@method_literals)['blank']
assert_evaluates_false(VariableLookup.new('hash'), '==', blank_literal)
assert_evaluates_false(VariableLookup.parse('hash'), '==', blank_literal)
end
# Tests for empty? comparison without ActiveSupport
@@ -322,7 +311,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_string'] = ''
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
assert_evaluates_true(VariableLookup.new('empty_string'), '==', empty_literal)
assert_evaluates_true(VariableLookup.parse('empty_string'), '==', empty_literal)
end
def test_empty_with_whitespace_string_not_empty
@@ -332,7 +321,7 @@ class ConditionUnitTest < Minitest::Test
@context['whitespace'] = ' '
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
assert_evaluates_false(VariableLookup.new('whitespace'), '==', empty_literal)
assert_evaluates_false(VariableLookup.parse('whitespace'), '==', empty_literal)
end
def test_empty_with_empty_array
@@ -341,7 +330,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_array'] = []
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
assert_evaluates_true(VariableLookup.new('empty_array'), '==', empty_literal)
assert_evaluates_true(VariableLookup.parse('empty_array'), '==', empty_literal)
end
def test_empty_with_empty_hash
@@ -350,7 +339,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_hash'] = {}
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
assert_evaluates_true(VariableLookup.new('empty_hash'), '==', empty_literal)
assert_evaluates_true(VariableLookup.parse('empty_hash'), '==', empty_literal)
end
def test_nil_is_not_empty
@@ -360,7 +349,7 @@ class ConditionUnitTest < Minitest::Test
@context['nil_value'] = nil
empty_literal = Condition.class_variable_get(:@@method_literals)['empty']
assert_evaluates_false(VariableLookup.new('nil_value'), '==', empty_literal)
assert_evaluates_false(VariableLookup.parse('nil_value'), '==', empty_literal)
end
private
+24 -87
View File
@@ -5,119 +5,56 @@ require 'test_helper'
class ParseContextUnitTest < Minitest::Test
include Liquid
def test_safe_parse_expression_with_variable_lookup
parser_strict = strict_parse_context.new_parser('product.title')
result_strict = strict_parse_context.safe_parse_expression(parser_strict)
def test_parser_expression_with_variable_lookup
parser = parse_context.new_parser('product.title')
result = parser.expression
parser_strict2 = strict2_parse_context.new_parser('product.title')
result_strict2 = strict2_parse_context.safe_parse_expression(parser_strict2)
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)
assert_equal('product', result.name)
assert_equal(['title'], result.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('')
def test_parser_expression_raises_syntax_error_for_invalid_expression
parser = 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 = assert_raises(Liquid::SyntaxError) do
parser.expression
end
assert_match(/is not a valid expression/, error_strict2.message)
assert_match(/is not a valid expression/, error.message)
end
def test_parse_expression_with_variable_lookup
result_strict = strict_parse_context.parse_expression('product.title')
result = parse_context.new_parser('product.title').expression
assert_instance_of(VariableLookup, result_strict)
assert_equal('product', result_strict.name)
assert_equal(['title'], result_strict.lookups)
error = assert_raises(Liquid::InternalError) do
strict2_parse_context.parse_expression('product.title')
end
assert_match(/unsafe parse_expression cannot be used in strict2 mode/, error.message)
assert_instance_of(VariableLookup, result)
assert_equal('product', result.name)
assert_equal(['title'], result.lookups)
end
def test_parse_expression_with_safe_true
result_strict = strict_parse_context.parse_expression('product.title', safe: true)
def test_parser_expression_advances_parser_pointer
parser = parse_context.new_parser('foo, bar')
assert_instance_of(VariableLookup, result_strict)
assert_equal('product', result_strict.name)
assert_equal(['title'], result_strict.lookups)
result_strict2 = strict2_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)
end
def test_parse_expression_with_empty_string
result_strict = strict_parse_context.parse_expression('')
assert_nil(result_strict)
error = assert_raises(Liquid::InternalError) do
strict2_parse_context.parse_expression('')
end
assert_match(/unsafe parse_expression cannot be used in strict2 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)
end
def test_safe_parse_expression_advances_parser_pointer
parser = strict2_parse_context.new_parser('foo, bar')
# safe_parse_expression consumes "foo"
first_result = strict2_parse_context.safe_parse_expression(parser)
# parser.expression consumes "foo"
first_result = parser.expression
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)
# parser.expression consumes "bar"
second_result = parser.expression
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)
assert_nil(result)
end
private
def strict_parse_context
@strict_parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :strict),
)
end
def strict2_parse_context
@strict2_parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :strict2),
def parse_context
@parse_context ||= ParseContext.new(
environment: Environment.build,
)
end
end
+69 -16
View File
@@ -45,40 +45,93 @@ class ParserUnitTest < Minitest::Test
assert_equal(false, p.look(:number, 1))
end
def test_expressions
def test_expression_string
p = new_parser("hi.there hi?[5].there? hi.there.bob")
assert_equal('hi.there', p.expression)
assert_equal('hi?[5].there?', p.expression)
assert_equal('hi.there.bob', p.expression)
assert_equal('hi.there', p.expression_string)
assert_equal('hi?[5].there?', p.expression_string)
assert_equal('hi.there.bob', p.expression_string)
p = new_parser("567 6.0 'lol' \"wut\"")
assert_equal('567', p.expression)
assert_equal('6.0', p.expression)
assert_equal("'lol'", p.expression)
assert_equal('"wut"', p.expression)
assert_equal('567', p.expression_string)
assert_equal('6.0', p.expression_string)
assert_equal("'lol'", p.expression_string)
assert_equal('"wut"', p.expression_string)
end
def test_expression
p = new_parser("hi.there hi?[5].there? hi.there.bob")
v1 = p.expression
v2 = p.expression
v3 = p.expression
assert(v1.is_a?(VariableLookup) && v1.name == 'hi' && v1.lookups[0] == 'there')
assert(v2.is_a?(VariableLookup) && v2.name == 'hi?' && v2.lookups[0] == 5)
assert(v3.is_a?(VariableLookup) && v3.name == 'hi' && v3.lookups[0] == 'there')
p = new_parser("567 6.0 'lol' \"wut\" true false (0..5)")
assert_equal(567, p.expression)
assert_equal(6.0, p.expression)
assert_equal('lol', p.expression)
assert_equal('wut', p.expression)
assert_equal(true, p.expression)
assert_equal(false, p.expression)
assert_equal(0..5, p.expression)
end
def test_number
p = new_parser('-1 0 1 2.0')
assert_equal(-1, p.number)
assert_equal(0, p.number)
assert_equal(1, p.number)
assert_equal(2.0, p.number)
end
def test_string
p = new_parser("'s1' \"s2\" 'this \"s3\"' \"that 's4'\"")
assert_equal('s1', p.string)
assert_equal('s2', p.string)
assert_equal('this "s3"', p.string)
assert_equal("that 's4'", p.string)
end
def test_unnamed_variable_lookup
p = new_parser('[key].title')
v = p.expression
assert(v.is_a?(VariableLookup))
assert(v.name.is_a?(VariableLookup))
assert_equal('key', v.name.name)
assert_equal('title', v.lookups[0])
end
def test_range_lookup
p = new_parser('(0..5) (a..b)')
assert_equal(0..5, p.expression)
r2 = p.expression
assert(r2.is_a?(RangeLookup))
assert_equal(1..4, r2.evaluate(Context.new({ 'a' => 1, 'b' => 4 })))
end
def test_ranges
p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal('(5..7)', p.expression)
assert_equal('(1.5..9.6)', p.expression)
assert_equal('(young..old)', p.expression)
assert_equal('(hi[5].wat..old)', p.expression)
assert_equal('(5..7)', p.expression_string)
assert_equal('(1.5..9.6)', p.expression_string)
assert_equal('(young..old)', p.expression_string)
assert_equal('(hi[5].wat..old)', p.expression_string)
end
def test_arguments
def test_argument_string
p = new_parser("filter: hi.there[5], keyarg: 7")
assert_equal('filter', p.consume(:id))
assert_equal(':', p.consume(:colon))
assert_equal('hi.there[5]', p.argument)
assert_equal('hi.there[5]', p.argument_string)
assert_equal(',', p.consume(:comma))
assert_equal('keyarg: 7', p.argument)
assert_equal('keyarg: 7', p.argument_string)
end
def test_invalid_expression
assert_raises(SyntaxError) do
p = new_parser("==")
p.expression
p.expression_string
end
end
+7 -9
View File
@@ -175,7 +175,7 @@ class PartialCacheUnitTest < Minitest::Test
assert_equal('some/path/my_partial', partial.name)
end
def test_includes_error_mode_into_template_cache
def test_cache_key
template_factory = StubTemplateFactory.new
context = Liquid::Context.build(
registers: {
@@ -184,16 +184,14 @@ class PartialCacheUnitTest < Minitest::Test
},
)
[:lax, :warn, :strict, :strict2].each do |error_mode|
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new(error_mode: error_mode),
)
end
Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new,
)
assert_equal(
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:strict2"],
["my_partial"],
context.registers[:cached_partials].keys,
)
end
+16 -46
View File
@@ -20,15 +20,9 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict) do
assert_template_result("one", template)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
end
assert_match(/Expected end_of_string but found/, error.message)
end
def test_case_when_with_trailing_element
@@ -41,15 +35,9 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict) do
assert_template_result("one", template)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
end
assert_match(/Expected end_of_string but found/, error.message)
end
def test_case_when_with_comma
@@ -62,9 +50,7 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict, :strict2) do
assert_template_result("one", template)
end
assert_template_result("one", template)
end
def test_case_when_with_or
@@ -77,9 +63,7 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%}
LIQUID
with_error_modes(:lax, :strict, :strict2) do
assert_template_result("one", template)
end
assert_template_result("one", template)
end
def test_case_when_empty
@@ -92,14 +76,12 @@ class CaseTagUnitTest < Minitest::Test
{%- 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
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
def test_case_with_invalid_expression
@@ -113,15 +95,9 @@ class CaseTagUnitTest < Minitest::Test
LIQUID
assigns = { 'foo' => { 'bar' => 'baz' } }
with_error_modes(:lax, :strict) do
assert_template_result("one", template, assigns)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
assert_match(/Unexpected character =/, error.message)
end
def test_case_when_with_invalid_expression
@@ -135,14 +111,8 @@ class CaseTagUnitTest < Minitest::Test
LIQUID
assigns = { 'foo' => { 'bar' => 'baz' } }
with_error_modes(:lax, :strict) do
assert_template_result("one", template, assigns)
end
error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
assert_match(/Unexpected character =/, error.message)
end
end
+40 -80
View File
@@ -7,20 +7,20 @@ class VariableUnitTest < Minitest::Test
def test_variable
var = create_variable('hello')
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
end
def test_filters
var = create_variable('hello | textileze')
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['textileze', []]], var.filters)
var = create_variable('hello | textileze | paragraph')
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable(%( hello | strftime: '%Y'))
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['strftime', ['%Y']]], var.filters)
var = create_variable(%( 'typo' | link_to: 'Typo', true ))
@@ -44,11 +44,11 @@ class VariableUnitTest < Minitest::Test
assert_equal([['repeat', [3, 3, 3]]], var.filters)
var = create_variable(%( hello | strftime: '%Y, okay?'))
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['strftime', ['%Y, okay?']]], var.filters)
var = create_variable(%( hello | things: "%Y, okay?", 'the other one'))
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters)
end
@@ -60,24 +60,18 @@ class VariableUnitTest < Minitest::Test
def test_filters_without_whitespace
var = create_variable('hello | textileze | paragraph')
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable('hello|textileze|paragraph')
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable("hello|replace:'foo','bar'|textileze")
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters)
end
def test_symbol
var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax)
assert_equal(VariableLookup.new('http://disney.com/logo.gif'), var.name)
assert_equal([['image', ['med']]], var.filters)
end
def test_string_to_filter
var = create_variable("'http://disney.com/logo.gif' | image: 'med' ")
assert_equal('http://disney.com/logo.gif', var.name)
@@ -105,14 +99,12 @@ class VariableUnitTest < Minitest::Test
end
def test_dashes
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
assert_equal(VariableLookup.parse('foo-bar'), create_variable('foo-bar').name)
assert_equal(VariableLookup.parse('foo-bar-2'), create_variable('foo-bar-2').name)
with_error_modes(:strict) do
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
end
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
end
def test_string_with_special_chars
@@ -122,79 +114,47 @@ class VariableUnitTest < Minitest::Test
def test_string_dot
var = create_variable(%( test.test ))
assert_equal(VariableLookup.new('test.test'), var.name)
assert_equal(VariableLookup.parse('test.test'), var.name)
end
def test_filter_with_keyword_arguments
var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye'))
assert_equal(VariableLookup.new('hello'), var.name)
assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters)
end
def test_lax_filter_argument_parsing
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
assert_equal(VariableLookup.new('number_of_comments'), var.name)
assert_equal([['pluralize', ['comment', 'comments']]], var.filters)
def test_filter_argument_parsing
# optional colon
var = create_variable(%(n | f1 | f2:))
assert_equal([['f1', []], ['f2', []]], var.filters)
# missing does not throws error
create_variable(%(n | f1: ,), error_mode: :lax)
create_variable(%(n | f1: ,| f2), error_mode: :lax)
# missing argument throws error
assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2)) }
# arg does not require colon, but ignores args :O, also ignores first kwarg since it splits on ':'
var = create_variable(%(n | f1 1 | f2 k1: v1), error_mode: :lax)
assert_equal([['f1', []], ['f2', [VariableLookup.new('v1')]]], var.filters)
# arg requires colon
assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
# trailing comma doesn't throw
create_variable(%(n | f1: 1, 2, 3, | f2:))
# missing comma throws error
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3)) }
# positional and kwargs parsing
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2), error_mode: :lax)
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2))
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
# positional and kwargs mixed
var = create_variable(%(n | filter: 'a', 'b', key1: 1, key2: 2, 'c'))
assert_equal([["filter", ["a", "b", "c"], { "key1" => 1, "key2" => 2 }]], var.filters)
# positional and kwargs intermixed (pos1, key1: val1, pos2)
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"), error_mode: :lax)
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"))
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
end
def test_strict_filter_argument_parsing
with_error_modes(:strict) do
assert_raises(SyntaxError) do
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
end
end
end
def test_strict2_filter_argument_parsing
with_error_modes(:strict2) do
# optional colon
var = create_variable(%(n | f1 | f2:))
assert_equal([['f1', []], ['f2', []]], var.filters)
# missing argument throws error
assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
assert_raises(SyntaxError) { create_variable(%(n | f1: ,| f2)) }
# arg requires colon
assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
# trailing comma doesn't throw
create_variable(%(n | f1: 1, 2, 3, | f2:))
# missing comma throws error
assert_raises(SyntaxError) { create_variable(%(n | filter: 1 2, 3)) }
# positional and kwargs parsing
var = create_variable(%(n | filter: 1, 2, 3 | filter2: k1: 1, k2: 2))
assert_equal([['filter', [1, 2, 3]], ['filter2', [], { "k1" => 1, "k2" => 2 }]], var.filters)
# positional and kwargs mixed
var = create_variable(%(n | filter: 'a', 'b', key1: 1, key2: 2, 'c'))
assert_equal([["filter", ["a", "b", "c"], { "key1" => 1, "key2" => 2 }]], var.filters)
# positional and kwargs intermixed (pos1, key1: val1, pos2)
var = create_variable(%(n | link_to: class: "black", "https://example.com", title: "title"))
assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
# string key throws
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
end
# string key throws
assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
end
def test_output_raw_source_of_variable
@@ -203,7 +163,7 @@ class VariableUnitTest < Minitest::Test
end
def test_variable_lookup_interface
lookup = VariableLookup.new('a.b.c')
lookup = VariableLookup.parse('a.b.c')
assert_equal('a', lookup.name)
assert_equal(['b', 'c'], lookup.lookups)
end