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 end
group :spec do 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 gem 'activesupport', require: false
end end
+42
View File
@@ -1,5 +1,47 @@
# Liquid Change Log # 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 ## 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] * 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] * 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`. 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 ### 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. 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
end end
desc('runs test suite with lax, strict, and strict2 parsers') desc('runs test suite')
task :test do 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'].reenable
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby' 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'].reenable
Rake::Task['integration_test'].invoke Rake::Task['integration_test'].invoke
end end
@@ -78,24 +61,11 @@ task release: :build do
end end
namespace :benchmark do namespace :benchmark do
desc "Run the liquid benchmark with lax parsing" desc "Run the liquid benchmark"
task :lax do task :run do
ruby "./performance/benchmark.rb lax" ruby "./performance/benchmark.rb"
end 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" desc "Run unit benchmarks"
namespace :unit do namespace :unit do
task :all do task :all do
@@ -126,11 +96,6 @@ namespace :profile do
task :run do task :run do
ruby "./performance/profile.rb" ruby "./performance/profile.rb"
end end
desc "Run the liquid profile/performance coverage with strict parsing"
task :strict do
ruby "./performance/profile.rb strict"
end
end end
namespace :memory_profile do namespace :memory_profile do
+3 -2
View File
@@ -48,8 +48,9 @@ module Liquid
@@operators @@operators
end end
def self.parse_expression(parse_context, markup, safe: false) def self.parse_expression(parser)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe) markup = parser.expression_string
@@method_literals[markup] || parser.unsafe_parse_expression(markup)
end end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
+3 -7
View File
@@ -60,10 +60,6 @@ module Liquid
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
def warnings
@warnings ||= []
end
def strainer def strainer
@strainer ||= @environment.create_strainer(self, @filters) @strainer ||= @environment.create_strainer(self, @filters)
end end
@@ -157,7 +153,6 @@ module Liquid
subcontext.filters = @filters subcontext.filters = @filters
subcontext.strainer = nil subcontext.strainer = nil
subcontext.errors = errors subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags subcontext.disabled_tags = @disabled_tags
end end
end end
@@ -180,7 +175,8 @@ module Liquid
# Example: # Example:
# products == empty #=> products.empty? # products == empty #=> products.empty?
def [](expression) def [](expression)
evaluate(Expression.parse(expression, @string_scanner)) @string_scanner.string = expression
evaluate(Parser.new(@string_scanner).expression)
end end
def key?(key) def key?(key)
@@ -244,7 +240,7 @@ module Liquid
protected protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags attr_writer :base_scope_depth, :errors, :strainer, :filters, :disabled_tags
private 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 Environment is the container for all configuration options of Liquid, such as
# the registered tags, filters, and the default error mode. # the registered tags, filters, and the default error mode.
class Environment 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. # The tags that are available to use in the template.
attr_accessor :tags attr_accessor :tags
@@ -33,17 +29,14 @@ module Liquid
# the template. # the template.
# @param file_system The default file system that is used # @param file_system The default file system that is used
# to load templates from. # 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 # @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions. # render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built. # @yieldparam environment [Environment] The environment instance that is being built.
# @return [Environment] The new environment instance. # @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 = new
ret.tags = tags if tags ret.tags = tags if tags
ret.file_system = file_system if file_system ret.file_system = file_system if file_system
ret.error_mode = error_mode if error_mode
ret.exception_renderer = exception_renderer if exception_renderer ret.exception_renderer = exception_renderer if exception_renderer
yield ret if block_given? yield ret if block_given?
ret.freeze ret.freeze
@@ -75,7 +68,6 @@ module Liquid
# @api private # @api private
def initialize def initialize
@tags = Tags::STANDARD_TAGS.dup @tags = Tags::STANDARD_TAGS.dup
@error_mode = :lax
@strainer_template = Class.new(StrainerTemplate).tap do |klass| @strainer_template = Class.new(StrainerTemplate).tap do |klass|
klass.add_filter(StandardFilters) klass.add_filter(StandardFilters)
end end
+14 -58
View File
@@ -11,9 +11,6 @@ module Liquid
'false' => false, 'false' => false,
'blank' => '', 'blank' => '',
'empty' => '', '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 }.freeze
DOT = ".".ord DOT = ".".ord
@@ -28,10 +25,6 @@ module Liquid
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/ FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
class << self 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) def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup return unless markup
@@ -56,71 +49,34 @@ module Liquid
def inner_parse(markup, ss, cache) def inner_parse(markup, ss, cache)
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
return RangeLookup.parse( start_markup = Regexp.last_match(1)
Regexp.last_match(1), end_markup = Regexp.last_match(2)
Regexp.last_match(2), start_obj = parse(start_markup, ss, cache)
ss, end_obj = parse(end_markup, ss, cache)
cache, return RangeLookup.create(
start_obj,
end_obj,
start_markup,
end_markup,
) )
end end
if (num = parse_number(markup, ss)) if (num = parse_number(markup))
num num
else else
VariableLookup.parse(markup, ss, cache) VariableLookup.parse(markup, ss, cache)
end end
end end
def parse_number(markup, ss) def parse_number(markup)
# check if the markup is simple integer or float # check if the markup is simple integer or float
case markup case markup
when INTEGER_REGEX when INTEGER_REGEX
return Integer(markup, 10) Integer(markup, 10)
when FLOAT_REGEX when FLOAT_REGEX
return markup.to_f 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
else else
# number ends with a dot "123." false
markup.byteslice(0, first_dot_pos).to_f
end end
end end
end end
+3 -25
View File
@@ -3,14 +3,13 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth 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) def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default) @environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@warnings = []
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive # constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them # This StringScanner will be shared by all of them
@@ -38,7 +37,7 @@ module Liquid
def new_parser(input) def new_parser(input)
@string_scanner.string = input @string_scanner.string = input
Parser.new(@string_scanner) Parser.new(@string_scanner, @expression_cache)
end end
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false) def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
@@ -50,30 +49,9 @@ module Liquid
) )
end 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) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || @environment.error_mode
end end
def partial_options def partial_options
+103 -12
View File
@@ -2,9 +2,10 @@
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input, expression_cache = nil)
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input) @ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = Lexer.tokenize(ss) @cache = expression_cache
@tokens = Lexer.tokenize(@ss)
@p = 0 # pointer to current location @p = 0 # pointer to current location
end end
@@ -47,23 +48,75 @@ module Liquid
end end
def expression 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] token = @tokens[@p]
case token[0] case token[0]
when :id when :id
str = consume str = consume
str << variable_lookups str << variable_lookups_string
when :open_square when :open_square
str = consume.dup str = consume.dup
str << expression str << expression_string
str << consume(:close_square) str << consume(:close_square)
str << variable_lookups str << variable_lookups_string
when :string, :number when :string, :number
consume consume
when :open_round when :open_round
consume consume
first = expression first = expression_string
consume(:dotdot) consume(:dotdot)
last = expression last = expression_string
consume(:close_round) consume(:close_round)
"(#{first}..#{last})" "(#{first}..#{last})"
else else
@@ -71,23 +124,23 @@ module Liquid
end end
end end
def argument def argument_string
str = +"" str = +""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' ' str << consume << consume << ' '
end end
str << expression str << expression_string
str str
end end
def variable_lookups def variable_lookups_string
str = +"" str = +""
loop do loop do
if look(:open_square) if look(:open_square)
str << consume str << consume
str << expression str << expression_string
str << consume(:close_square) str << consume(:close_square)
elsif look(:dot) elsif look(:dot)
str << consume str << consume
@@ -98,5 +151,43 @@ module Liquid
end end
str str
end 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
end end
+5 -64
View File
@@ -2,75 +2,16 @@
module Liquid module Liquid
module ParserSwitching 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) def parse_with_selected_parser(markup)
case parse_context.error_mode parse_markup(markup)
when :rigid then rigid_warn && strict2_parse_with_error_context(markup) rescue SyntaxError => e
when :strict2 then strict2_parse_with_error_context(markup) e.line_number = line_number
when :strict then strict_parse_with_error_context(markup) e.markup_context = markup_context(markup)
when :lax then lax_parse(markup) raise e
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
end end
private 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) def markup_context(markup)
"in \"#{markup.strip}\"" "in \"#{markup.strip}\""
end end
+1 -1
View File
@@ -4,7 +4,7 @@ module Liquid
class PartialCache class PartialCache
def self.load(template_name, context:, parse_context:) def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials] 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] cached = cached_partials[cache_key]
return cached if cached return cached if cached
+3 -3
View File
@@ -2,15 +2,15 @@
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup, string_scanner, cache = nil) def self.create(start_obj, end_obj, start_markup = nil, end_markup = nil)
start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup, string_scanner, cache)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
begin begin
start_obj.to_i..end_obj.to_i start_obj.to_i..end_obj.to_i
rescue NoMethodError 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 = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i) invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr if invalid_expr
-10
View File
@@ -65,15 +65,5 @@ module Liquid
def blank? def blank?
false false
end 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
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 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. # @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left attr_reader :blocks, :left
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
@@ -86,39 +83,23 @@ module Liquid
private private
def strict2_parse(markup) def parse_markup(markup)
parser = @parse_context.new_parser(markup) parser = @parse_context.new_parser(markup)
@left = safe_parse_expression(parser) @left = parser.expression
parser.consume(:end_of_string) parser.consume(:end_of_string)
end 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) def record_when_condition(markup)
body = new_body body = new_body
if strict2_mode? parse_when(markup, body)
parse_strict2_when(markup, body)
else
parse_lax_when(markup, body)
end
end end
def parse_strict2_when(markup, body) def parse_when(markup, body)
parser = @parse_context.new_parser(markup) parser = @parse_context.new_parser(markup)
loop do loop do
expr = Condition.parse_expression(parse_context, parser.expression, safe: true) expr = Condition.parse_expression(parser)
block = Condition.new(@left, '==', expr) block = Condition.new(@left, '==', expr)
block.attach(body) block.attach(body)
@blocks << block @blocks << block
@@ -129,20 +110,6 @@ module Liquid
parser.consume(:end_of_string) parser.consume(:end_of_string)
end 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) def record_else_condition(markup)
unless markup.strip.empty? unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else") raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
+4 -35
View File
@@ -15,8 +15,6 @@ module Liquid
# @liquid_syntax # @liquid_syntax
# {% cycle string, string, ... %} # {% cycle string, string, ... %}
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/ UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
attr_reader :variables attr_reader :variables
@@ -56,21 +54,21 @@ module Liquid
private private
# cycle [name:] expression(, expression)* # cycle [name:] expression(, expression)*
def strict2_parse(markup) def parse_markup(markup)
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
@variables = [] @variables = []
raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string) 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) if p.look(:colon)
# cycle name: expr1, expr2, ... # cycle name: expr1, expr2, ...
@name = first_expression @name = first_expression
@is_named = true @is_named = true
p.consume(:colon) p.consume(:colon)
# After the colon, parse the first variable (required for named cycles) # 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 else
# cycle expr1, expr2, ... # cycle expr1, expr2, ...
@variables << maybe_dup_lookup(first_expression) @variables << maybe_dup_lookup(first_expression)
@@ -80,7 +78,7 @@ module Liquid
while p.consume?(:comma) while p.consume?(:comma)
break if p.look(:end_of_string) break if p.look(:end_of_string)
@variables << maybe_dup_lookup(safe_parse_expression(p)) @variables << maybe_dup_lookup(p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -91,35 +89,6 @@ module Liquid
end end
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, # 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 # 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) # 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 range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order. # @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
@@ -72,28 +70,13 @@ module Liquid
protected protected
def lax_parse(markup) def parse_markup(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)
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression collection_name = p.expression_string
@collection_name = parse_expression(collection_name, safe: true) @collection_name = p.unsafe_parse_expression(collection_name)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
@@ -104,17 +87,13 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume(:colon) p.consume(:colon)
set_attribute(attribute, p.expression, safe: true) set_attribute(attribute, p)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
private private
def strict2_parse(markup)
strict_parse(markup)
end
def collection_segment(context) def collection_segment(context)
offsets = context.registers[:for] ||= {} offsets = context.registers[:for] ||= {}
@@ -178,16 +157,17 @@ module Liquid
output output
end end
def set_attribute(key, expr, safe: false) def set_attribute(key, p)
expr = p.expression_string
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
:continue :continue
else else
parse_expression(expr, safe: safe) p.unsafe_parse_expression(expr)
end end
when 'limit' when 'limit'
@limit = parse_expression(expr, safe: safe) @limit = p.unsafe_parse_expression(expr)
end end
end end
+5 -33
View File
@@ -14,10 +14,6 @@ module Liquid
# @liquid_syntax_keyword condition The condition to evaluate. # @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render if the condition is met. # @liquid_syntax_keyword expression The expression to render if the condition is met.
class If < Block 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 attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
@@ -66,10 +62,6 @@ module Liquid
private private
def strict2_parse(markup)
strict_parse(markup)
end
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else' block = if tag == 'else'
ElseCondition.new ElseCondition.new
@@ -81,31 +73,11 @@ module Liquid
block.attach(new_body) block.attach(new_body)
end end
def parse_expression(markup, safe: false) def parse_expression(parser)
Condition.parse_expression(parse_context, markup, safe: safe) Condition.parse_expression(parser)
end end
def lax_parse(markup) def parse_markup(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)
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
condition = parse_binary_comparisons(p) condition = parse_binary_comparisons(p)
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -124,9 +96,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = parse_expression(p.expression, safe: true) a = parse_expression(p)
if (op = p.consume?(:comparison)) if (op = p.consume?(:comparison))
b = parse_expression(p.expression, safe: true) b = parse_expression(p)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
+4 -30
View File
@@ -20,9 +20,6 @@ module Liquid
class Include < Tag class Include < Tag
prepend Tag::Disableable 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 attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
@@ -84,11 +81,11 @@ module Liquid
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def strict2_parse(markup) def parse_markup(markup)
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
@template_name_expr = safe_parse_expression(p) @template_name_expr = p.expression
@variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with") @variable_name_expr = p.expression if p.id?("for") || p.id?("with")
@alias_name = p.consume(:id) if p.id?("as") @alias_name = p.consume(:id) if p.id?("as")
p.consume?(:comma) p.consume?(:comma)
@@ -97,36 +94,13 @@ module Liquid
while p.look(:id) while p.look(:id)
key = p.consume key = p.consume
p.consume(:colon) p.consume(:colon)
@attributes[key] = safe_parse_expression(p) @attributes[key] = p.expression
p.consume?(:comma) p.consume?(:comma)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end 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 class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children 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. # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag class Render < Tag
FOR = 'for' FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" disable_tags "include"
@@ -85,12 +84,12 @@ module Liquid
end end
# render (string) (with|for expression)? (as id)? (key: value)* # render (string) (with|for expression)? (as id)? (key: value)*
def strict2_parse(markup) def parse_markup(markup)
p = @parse_context.new_parser(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") 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") @alias_name = p.consume(:id) if p.id?("as")
@is_for_loop = (with_or_for == FOR) @is_for_loop = (with_or_for == FOR)
@@ -100,37 +99,15 @@ module Liquid
while p.look(:id) while p.look(:id)
key = p.consume key = p.consume
p.consume(:colon) p.consume(:colon)
@attributes[key] = safe_parse_expression(p) @attributes[key] = p.expression
p.consume?(:comma) p.consume?(:comma)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end end
def strict2_template_name(p) def template_name(p)
p.consume(:string) p.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
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor 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 offset: [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over. # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
attr_reader :variable_name, :collection_name, :attributes attr_reader :variable_name, :collection_name, :attributes
@@ -34,7 +33,7 @@ module Liquid
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end end
def strict2_parse(markup) def parse_markup(markup)
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
@@ -43,7 +42,7 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
end end
@collection_name = safe_parse_expression(p) @collection_name = p.expression
p.consume?(:comma) p.consume?(:comma)
@@ -55,30 +54,13 @@ module Liquid
end end
p.consume(:colon) p.consume(:colon)
@attributes[key] = safe_parse_expression(p) @attributes[key] = p.expression
p.consume?(:comma) p.consume?(:comma)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
end 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) def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
+1 -16
View File
@@ -16,25 +16,11 @@ module Liquid
# #
class Template class Template
attr_accessor :root, :name attr_accessor :root, :name
attr_reader :resource_limits, :warnings attr_reader :resource_limits
attr_reader :profiler attr_reader :profiler
class << self 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) def default_exception_renderer=(renderer)
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=") Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
Environment.default.exception_renderer = renderer Environment.default.exception_renderer = renderer
@@ -223,7 +209,6 @@ module Liquid
ParseContext.new(opts) ParseContext.new(opts)
end end
@warnings = parse_context.warnings
parse_context parse_context
end end
+7 -64
View File
@@ -30,7 +30,7 @@ module Liquid
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup) parse_with_selected_parser(markup)
end end
def raw def raw
@@ -41,58 +41,17 @@ module Liquid
"in \"{{#{markup}}}\"" "in \"{{#{markup}}}\""
end end
def lax_parse(markup) def parse_markup(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)
@filters = [] @filters = []
p = @parse_context.new_parser(markup) p = @parse_context.new_parser(markup)
return if p.look(:end_of_string) return if p.look(:end_of_string)
@name = parse_context.safe_parse_expression(p) @name = p.expression
while p.consume?(:pipe) @filters << parse_filter_expressions(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
p.consume(:end_of_string) p.consume(:end_of_string)
end 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) def render(context)
obj = context.evaluate(@name) obj = context.evaluate(@name)
@@ -133,22 +92,6 @@ module Liquid
private 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. # Surprisingly, positional and keyword arguments can be mixed.
# #
# filter = filtername [":" filterargs?] # filter = filtername [":" filterargs?]
@@ -156,7 +99,7 @@ module Liquid
# argument = (positional_argument | keyword_argument) # argument = (positional_argument | keyword_argument)
# positional_argument = expression # positional_argument = expression
# keyword_argument = id ":" expression # keyword_argument = id ":" expression
def strict2_parse_filter_expressions(p) def parse_filter_expressions(p)
filtername = p.consume(:id) filtername = p.consume(:id)
filter_args = [] filter_args = []
keyword_args = {} keyword_args = {}
@@ -178,10 +121,10 @@ module Liquid
if p.look(:id) && p.look(:colon, 1) if p.look(:id) && p.look(:colon, 1)
key = p.consume(:id) key = p.consume(:id)
p.consume(:colon) p.consume(:colon)
value = parse_context.safe_parse_expression(p) value = p.expression
keyword_arguments[key] = value keyword_arguments[key] = value
else else
positional_arguments << parse_context.safe_parse_expression(p) positional_arguments << p.expression
end end
end end
+11 -9
View File
@@ -7,10 +7,6 @@ module Liquid
attr_reader :name, :lookups attr_reader :name, :lookups
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil) 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) lookups = markup.scan(VariableParser)
name = lookups.shift name = lookups.shift
@@ -21,12 +17,10 @@ module Liquid
cache, cache,
) )
end end
@name = name
@lookups = lookups command_flags = 0
@command_flags = 0
@lookups.each_index do |i| lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']') if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse( lookups[i] = Expression.parse(
@@ -35,9 +29,17 @@ module Liquid
cache, cache,
) )
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i command_flags |= 1 << i
end end
end end
new(name, lookups, command_flags)
end
def initialize(name, lookups, command_flags)
@name = name
@lookups = lookups
@command_flags = command_flags
end end
def lookup_command?(lookup_index) def lookup_command?(lookup_index)
-1
View File
@@ -4,7 +4,6 @@ require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
RubyVM::YJIT.enable if defined?(RubyVM::YJIT) RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
-2
View File
@@ -53,8 +53,6 @@ class Profiler
end end
end end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
runner = ThemeRunner.new runner = ThemeRunner.new
Profiler.run do |x| Profiler.run do |x|
x.profile('parse') { runner.compile } x.profile('parse') { runner.compile }
-1
View File
@@ -3,7 +3,6 @@
require 'stackprof' require 'stackprof'
require_relative 'theme_runner' require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
profiler.run profiler.run
+32 -21
View File
@@ -6,7 +6,7 @@ require "benchmark/ips"
require 'liquid' require 'liquid'
RubyVM::YJIT.enable RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
STRING_MARKUPS = [ STRING_MARKUPS = [
"\"foo\"", "\"foo\"",
@@ -45,22 +45,14 @@ NUMBER_MARKUPS = [
RANGE_MARKUPS = [ RANGE_MARKUPS = [
"(1..30)", "(1..30)",
"(1...30)",
"(1..30..5)",
"(1.0...30.0)",
"(1.........30)",
"(1..foo)", "(1..foo)",
"(foo..30)", "(foo..30)",
"(foo..bar)", "(foo..bar)",
"(foo...bar...100)",
"(foo...bar...100.0)",
] ]
LITERAL_MARKUPS = [ LITERAL_MARKUPS = [
nil,
'nil', 'nil',
'null', 'null',
'',
'true', 'true',
'false', 'false',
'blank', 'blank',
@@ -75,20 +67,39 @@ MARKUPS = {
"range" => RANGE_MARKUPS, "range" => RANGE_MARKUPS,
} }
Benchmark.ips do |x| module Liquid
x.config(time: 5, warmup: 5) Benchmark.ips do |x|
x.config(time: 5, warmup: 5)
MARKUPS.each do |type, markups| ss = StringScanner.new('')
x.report("Liquid::Expression#parse: #{type}") do
markups.each do |markup| MARKUPS.each do |type, markups|
Liquid::Expression.parse(markup) 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 end
end end
x.report("Liquid::Expression#parse: all") do
MARKUPS.values.flatten.each do |markup|
Liquid::Expression.parse(markup)
end
end
end end
+4 -3
View File
@@ -6,7 +6,7 @@ require "benchmark/ips"
require 'liquid' require 'liquid'
RubyVM::YJIT.enable RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
EXPRESSIONS = [ EXPRESSIONS = [
"foo[1..2].baz", "foo[1..2].baz",
@@ -31,11 +31,12 @@ EXPRESSIONS = [
Benchmark.ips do |x| Benchmark.ips do |x|
x.config(time: 10, warmup: 5) x.config(time: 10, warmup: 5)
ss = StringScanner.new('')
x.report("Liquid::Lexer#tokenize") do x.report("Liquid::Lexer#tokenize") do
EXPRESSIONS.each do |expr| EXPRESSIONS.each do |expr|
l = Liquid::Lexer.new(expr) ss.string = expr
l.tokenize Liquid::Lexer.tokenize(ss)
end end
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 # 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 # 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 # Compile a template string into a Liquid::Template
LiquidSpec.compile do |ctx, source, options| LiquidSpec.compile do |ctx, source, options|
# Force strict mode
options = { error_mode: :strict }.merge(options)
ctx[:template] = Liquid::Template.parse(source, **options) ctx[:template] = Liquid::Template.parse(source, **options)
end end
+2 -4
View File
@@ -39,13 +39,11 @@ class AssignTest < Minitest::Test
assert_match_syntax_error(/assign/, '{% assign foo not values %}.') assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
end end
def test_assign_uses_error_mode def test_assign_throws_on_unsupported_syntax
assert_match_syntax_error( assert_match_syntax_error(
"Expected dotdot but found pipe in ", "Expected dotdot but found pipe",
"{% assign foo = ('X' | downcase) %}", "{% assign foo = ('X' | downcase) %}",
error_mode: :strict,
) )
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
end end
def test_expression_with_whitespace_in_square_brackets def test_expression_with_whitespace_in_square_brackets
+3 -5
View File
@@ -632,11 +632,9 @@ class ContextTest < Minitest::Test
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
with_error_modes(:strict) do context = Context.new
context = Context.new context.key?('unknown')
context.key?('unknown') assert_empty(context.errors)
assert_empty(context.errors)
end
end end
def test_key_lookup_will_raise_for_missing_keys_when_strict_variables_is_enabled 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 end
def test_unrecognized_operator def test_unrecognized_operator
with_error_modes(:strict) do assert_raises(SyntaxError) do
assert_raises(SyntaxError) do Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ')
end
end end
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 def test_with_line_numbers_adds_numbers_to_parser_errors
source = <<~LIQUID source = <<~LIQUID
foobar foobar
@@ -104,25 +95,6 @@ class ErrorHandlingTest < Minitest::Test
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source) assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
end 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 def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse( Liquid::Template.parse(
@@ -133,7 +105,6 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict,
line_numbers: true, line_numbers: true,
) )
end end
@@ -157,34 +128,16 @@ class ErrorHandlingTest < Minitest::Test
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do 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 end
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message) assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict) Liquid::Template.parse('{{%%%}}')
end end
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message) assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
end 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 # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate def test_exceptions_propagate
assert_raises(Exception) do 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("-17.42", "{{ -17.42 }}")
assert_template_result("2.5", "{{ 2.5 }}") 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") assert_expression_result(1.5, "1.5")
# this is a unfortunate quirky behavior of Liquid # this is a unfortunate quirky behavior of Liquid
@@ -51,24 +46,11 @@ class ExpressionTest < Minitest::Test
"{{ (false..true) }}", "{{ (false..true) }}",
) )
assert_match_syntax_error( 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) }}", "{{ ((1..2)..3) }}",
) )
end 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 def test_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled 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 Liquid::Template.parse(template, expression_cache: cache).render
assert_equal( assert_equal(
["1", "2", "x", "y"], [],
cache.to_a.map { _1[0] }.sort, cache.to_a.map { _1[0] }.sort,
) )
end end
@@ -109,7 +91,7 @@ class ExpressionTest < Minitest::Test
cache = parse_context.instance_variable_get(:@expression_cache) cache = parse_context.instance_variable_get(:@expression_cache)
assert_equal( assert_equal(
["1", "2", "x", "y"], [],
cache.to_a.map { _1[0] }.sort, cache.to_a.map { _1[0] }.sort,
) )
end end
@@ -130,7 +112,7 @@ class ExpressionTest < Minitest::Test
Liquid::Template.parse(template, expression_cache: cache).render Liquid::Template.parse(template, expression_cache: cache).render
assert_equal( assert_equal(
["1", "2", "x", "y"], [],
cache.to_a.map { _1[0] }.sort, cache.to_a.map { _1[0] }.sort,
) )
end end
@@ -152,30 +134,30 @@ class ExpressionTest < Minitest::Test
assert(parse_context.instance_variable_get(:@expression_cache).nil?) assert(parse_context.instance_variable_get(:@expression_cache).nil?)
end end
def test_safe_parse_with_variable_lookup def test_parser_expression_with_variable_lookup
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('product.title') parser = parse_context.new_parser('product.title')
result = Liquid::Expression.safe_parse(parser) result = parser.expression
assert_instance_of(Liquid::VariableLookup, result) assert_instance_of(Liquid::VariableLookup, result)
assert_equal('product', result.name) assert_equal('product', result.name)
assert_equal(['title'], result.lookups) assert_equal(['title'], result.lookups)
end end
def test_safe_parse_with_number def test_parser_expression_with_number
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('42') parser = parse_context.new_parser('42')
result = Liquid::Expression.safe_parse(parser) result = parser.expression
assert_equal(42, result) assert_equal(42, result)
end 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 parse_context = Liquid::ParseContext.new
parser = parse_context.new_parser('') parser = parse_context.new_parser('')
error = assert_raises(Liquid::SyntaxError) do error = assert_raises(Liquid::SyntaxError) do
Liquid::Expression.safe_parse(parser) parser.expression
end end
assert_match(/is not a valid expression/, error.message) 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 def test_error_on_empty_filter
assert(Template.parse("{{test}}")) assert(Template.parse("{{test}}"))
with_error_modes(:lax) do assert_raises(Liquid::SyntaxError) { Template.parse("{{|test}}") }
assert(Template.parse("{{|test}}")) assert_raises(Liquid::SyntaxError) { Template.parse("{{test |a|b|}}") }
end
with_error_modes(:strict) do
assert_raises(SyntaxError) { Template.parse("{{|test}}") }
assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") }
end
end end
def test_meaningless_parens_error def test_meaningless_parens_error
with_error_modes(:strict) do assert_raises(SyntaxError) do
assert_raises(SyntaxError) do markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" Template.parse("{% if #{markup} %} YES {% endif %}")
Template.parse("{% if #{markup} %} YES {% endif %}")
end
end end
end end
def test_unexpected_characters_syntax_error def test_unexpected_characters_syntax_error
with_error_modes(:strict) do assert_raises(SyntaxError) 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
markup = "true && false" 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" markup = "false || true"
assert_template_result('', "{% if #{markup} %} YES {% endif %}") Template.parse("{% if #{markup} %} YES {% endif %}")
end end
end end
@@ -92,32 +59,6 @@ class ParsingQuirksTest < Minitest::Test
end end
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 def test_blank_variable_markup
assert_template_result('', "{{}}") assert_template_result('', "{{}}")
end end
@@ -131,24 +72,4 @@ class ParsingQuirksTest < Minitest::Test
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true }) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end 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 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) assert_match(/Syntax Error in 'cycle' - Valid syntax: cycle \[name :\] var/, error.message)
end end
def test_cycle_tag_with_error_mode def test_cycle_tag_unsupported_legacy_quirk
# QuotedFragment is more permissive than what Parser#expression allows. # QuotedFragment is more permissive than what Parser#expression allows.
template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}" template1 = "{% assign 5 = 'b' %}{% cycle .5, .4 %}"
template2 = "{% cycle .5: 'a', 'b' %}" template2 = "{% cycle .5: 'a', 'b' %}"
with_error_modes(:lax, :strict) do error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
assert_template_result("b", template1) error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
assert_template_result("a", template2)
end
with_error_modes(:strict2) do expected_error = /Liquid syntax error: \[:dot, "."\] is not a valid expression/
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/ assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
end
end end
def test_cycle_with_trailing_elements def test_cycle_with_trailing_elements_legacy_syntax
assignments = "{% assign a = 'A' %}{% assign n = 'N' %}" assignments = "{% assign a = 'A' %}{% assign n = 'N' %}"
template1 = "#{assignments}{% cycle 'a' 'b', 'c' %}" template1 = "#{assignments}{% cycle 'a' 'b', 'c' %}"
@@ -121,29 +114,19 @@ class CycleTagTest < Minitest::Test
template4 = "#{assignments}{% cycle n e: 'a', 'b', 'c' %}" template4 = "#{assignments}{% cycle n e: 'a', 'b', 'c' %}"
template5 = "#{assignments}{% cycle n e 'a', 'b', 'c' %}" template5 = "#{assignments}{% cycle n e 'a', 'b', 'c' %}"
with_error_modes(:lax, :strict) do error1 = assert_raises(Liquid::SyntaxError) { Template.parse(template1) }
assert_template_result("a", template1) error2 = assert_raises(Liquid::SyntaxError) { Template.parse(template2) }
assert_template_result("a", template2) error3 = assert_raises(Liquid::SyntaxError) { Template.parse(template3) }
assert_template_result("a", template3) error4 = assert_raises(Liquid::SyntaxError) { Template.parse(template4) }
assert_template_result("N", template4) error5 = assert_raises(Liquid::SyntaxError) { Template.parse(template5) }
assert_template_result("N", template5)
end
with_error_modes(:strict2) do expected_error = /Expected end_of_string but found/
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/ assert_match(expected_error, error1.message)
assert_match(expected_error, error2.message)
assert_match(expected_error, error1.message) assert_match(expected_error, error3.message)
assert_match(expected_error, error2.message) assert_match(expected_error, error4.message)
assert_match(expected_error, error3.message) assert_match(expected_error, error5.message)
assert_match(expected_error, error4.message)
assert_match(expected_error, error5.message)
end
end end
def test_cycle_name_with_invalid_expression def test_cycle_name_with_invalid_expression
@@ -153,14 +136,8 @@ class CycleTagTest < Minitest::Test
{% endfor %} {% endfor %}
LIQUID LIQUID
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
refute_nil(Template.parse(template)) assert_match(/Unexpected character =/, error.message)
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
def test_cycle_variable_with_invalid_expression def test_cycle_variable_with_invalid_expression
@@ -170,13 +147,7 @@ class CycleTagTest < Minitest::Test
{% endfor %} {% endfor %}
LIQUID LIQUID
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
refute_nil(Template.parse(template)) assert_match(/Unexpected character =/, error.message)
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
end end
+16 -50
View File
@@ -204,23 +204,13 @@ class IncludeTagTest < Minitest::Test
) )
end end
def test_strict2_parsing_errors def test_parsing_errors_for_legacy_quirk
with_error_modes(:lax, :strict) do assert_syntax_error(
assert_template_result( '{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
'hello value1 value2', )
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}', assert_syntax_error(
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' }, '{% include "snippet" | filter %}',
) )
end
with_error_modes(:strict2) do
assert_syntax_error(
'{% include "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% include "snippet" | filter %}',
)
end
end end
def test_optional_commas def test_optional_commas
@@ -301,16 +291,10 @@ class IncludeTagTest < Minitest::Test
env = Liquid::Environment.build(file_system: TestFileSystem.new) env = Liquid::Environment.build(file_system: TestFileSystem.new)
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", 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 }}'))
end end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", 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 }}'))
end end
end end
@@ -365,7 +349,7 @@ class IncludeTagTest < Minitest::Test
file_system: StubFileSystem.new('simple' => 'simple'), 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) template.render(nil, strict_variables: true)
assert_equal([], template.errors) assert_equal([], template.errors)
@@ -404,39 +388,21 @@ class IncludeTagTest < Minitest::Test
def test_include_template_with_invalid_expression def test_include_template_with_invalid_expression
template = "{% include foo=>bar %}" template = "{% include foo=>bar %}"
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
refute_nil(Template.parse(template)) assert_match(/Unexpected character =/, error.message)
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
def test_include_with_invalid_expression def test_include_with_invalid_expression
template = '{% include "snippet" with foo=>bar %}' template = '{% include "snippet" with foo=>bar %}'
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
refute_nil(Template.parse(template)) assert_match(/Unexpected character =/, error.message)
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
def test_include_attribute_with_invalid_expression def test_include_attribute_with_invalid_expression
template = '{% include "snippet", key: foo=>bar %}' template = '{% include "snippet", key: foo=>bar %}'
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
refute_nil(Template.parse(template)) assert_match(/Unexpected character =/, error.message)
end
with_error_modes(:strict2) do
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
end # IncludeTagTest end # IncludeTagTest
+11 -35
View File
@@ -105,23 +105,13 @@ class RenderTagTest < Minitest::Test
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}") assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
end end
def test_strict2_parsing_errors def test_parsing_errors_legacy_syntax
with_error_modes(:lax, :strict) do assert_syntax_error(
assert_template_result( '{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
'hello value1 value2', )
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}', assert_syntax_error(
partials: { 'snippet' => 'hello {{ arg1 }} {{ arg2 }}' }, '{% render "snippet" | filter %}',
) )
end
with_error_modes(:strict2) do
assert_syntax_error(
'{% render "snippet" !!! arg1: "value1" ~~~ arg2: "value2" %}',
)
assert_syntax_error(
'{% render "snippet" | filter %}',
)
end
end end
def test_optional_commas def test_optional_commas
@@ -317,27 +307,13 @@ class RenderTagTest < Minitest::Test
def test_render_with_invalid_expression def test_render_with_invalid_expression
template = '{% render "snippet" with foo=>bar %}' template = '{% render "snippet" with foo=>bar %}'
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
with_error_modes(:lax, :strict) do assert_match(/Unexpected character =/, error.message)
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
end end
def test_render_attribute_with_invalid_expression def test_render_attribute_with_invalid_expression
template = '{% render "snippet", key: foo=>bar %}' template = '{% render "snippet", key: foo=>bar %}'
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
with_error_modes(:lax, :strict) do assert_match(/Unexpected character =/, error.message)
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
end end
end end
+32 -102
View File
@@ -188,29 +188,6 @@ class TableRowTest < Minitest::Test
assert_template_result(expected_output, template) assert_template_result(expected_output, template)
end 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 def test_table_row_handles_interrupts
assert_template_result( assert_template_result(
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n", "<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
@@ -259,7 +236,7 @@ class TableRowTest < Minitest::Test
) )
end end
def test_tablerow_with_cols_attribute_in_strict2_mode def test_tablerow_with_cols_attribute
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %} {% tablerow i in (1..6) cols: 3 %}{{ i }}{% endtablerow %}
LIQUID 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> <tr class="row2"><td class="col1">4</td><td class="col2">5</td><td class="col3">6</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_with_limit_attribute_in_strict2_mode def test_tablerow_with_limit_attribute
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %} {% tablerow i in (1..10) limit: 3 %}{{ i }}{% endtablerow %}
LIQUID 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> <td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_with_offset_attribute_in_strict2_mode def test_tablerow_with_offset_attribute
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %} {% tablerow i in (1..5) offset: 2 %}{{ i }}{% endtablerow %}
LIQUID 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> <td class="col1">3</td><td class="col2">4</td><td class="col3">5</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_with_range_attribute_in_strict2_mode def test_tablerow_with_range_attribute
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %} {% tablerow i in (1..3) range: (1..10) %}{{ i }}{% endtablerow %}
LIQUID 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> <td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_with_multiple_attributes_in_strict2_mode def test_tablerow_with_multiple_attributes
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %} {% tablerow i in (1..10) cols: 2, limit: 4, offset: 1 %}{{ i }}{% endtablerow %}
LIQUID LIQUID
@@ -331,12 +300,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr> <tr class="row2"><td class="col1">4</td><td class="col2">5</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_with_variable_collection_in_strict2_mode def test_tablerow_with_variable_collection
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %} {% tablerow n in numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID LIQUID
@@ -347,12 +314,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr> <tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
assert_template_result(expected, template, { 'numbers' => [1, 2, 3, 4] })
end
end end
def test_tablerow_with_dotted_access_in_strict2_mode def test_tablerow_with_dotted_access
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %} {% tablerow n in obj.numbers cols: 2 %}{{ n }}{% endtablerow %}
LIQUID LIQUID
@@ -363,12 +328,10 @@ class TableRowTest < Minitest::Test
<tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr> <tr class="row2"><td class="col1">3</td><td class="col2">4</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
assert_template_result(expected, template, { 'obj' => { 'numbers' => [1, 2, 3, 4] } })
end
end end
def test_tablerow_with_bracketed_access_in_strict2_mode def test_tablerow_with_bracketed_access
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %} {% tablerow n in obj["numbers"] cols: 2 %}{{ n }}{% endtablerow %}
LIQUID LIQUID
@@ -378,12 +341,10 @@ class TableRowTest < Minitest::Test
<td class="col1">10</td><td class="col2">20</td></tr> <td class="col1">10</td><td class="col2">20</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
assert_template_result(expected, template, { 'obj' => { 'numbers' => [10, 20] } })
end
end end
def test_tablerow_without_attributes_in_strict2_mode def test_tablerow_without_attributes
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in (1..3) %}{{ i }}{% endtablerow %} {% tablerow i in (1..3) %}{{ i }}{% endtablerow %}
LIQUID 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> <td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template)
assert_template_result(expected, template)
end
end end
def test_tablerow_without_in_keyword_in_strict2_mode def test_tablerow_without_in_keyword
template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}' template = '{% tablerow i (1..10) %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do error = assert_raises(SyntaxError) { Template.parse(template) }
error = assert_raises(SyntaxError) { Template.parse(template) } assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
assert_equal("Liquid syntax error: For loops require an 'in' clause in \"i (1..10)\"", error.message)
end
end 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 %}' template = '{% tablerow i in (1..10) invalid1: 5, invalid2: 10 %}{{ i }}{% endtablerow %}'
with_error_modes(:strict2) do error = assert_raises(SyntaxError) { Template.parse(template) }
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)
assert_equal("Liquid syntax error: Invalid attribute 'invalid1' in tablerow loop. Valid attributes are cols, limit, offset, and range in \"i in (1..10) invalid1: 5, invalid2: 10\"", error.message)
end
end end
def test_tablerow_with_empty_collection_in_strict2_mode def test_tablerow_with_empty_collection
template = <<~LIQUID.chomp template = <<~LIQUID.chomp
{% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %} {% tablerow i in empty_array cols: 2 %}{{ i }}{% endtablerow %}
LIQUID LIQUID
@@ -426,43 +381,18 @@ class TableRowTest < Minitest::Test
</tr> </tr>
OUTPUT OUTPUT
with_error_modes(:strict2) do assert_template_result(expected, template, { 'empty_array' => [] })
assert_template_result(expected, template, { 'empty_array' => [] })
end
end 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 %}' template = '{% tablerow i in (1..5) invalid_attr: 10 %}{{ i }}{% endtablerow %}'
error = assert_raises(SyntaxError) { Template.parse(template) }
expected = <<~OUTPUT assert_match(/Invalid attribute 'invalid_attr'/, error.message)
<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
end 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 %}' template = '{% tablerow i in (1..5) limit: foo=>bar %}{{ i }}{% endtablerow %}'
error = assert_raises(SyntaxError) { Template.parse(template) }
with_error_modes(:lax, :strict) do assert_match(/Unexpected character =/, error.message)
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
end end
end end
+1 -11
View File
@@ -44,16 +44,6 @@ class TemplateTest < Minitest::Test
assert_equal('from instance assigns', t.parse("{{ foo }}").render!) assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end 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 def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal('foo', t.render!) assert_equal('foo', t.render!)
@@ -259,7 +249,7 @@ class TemplateTest < Minitest::Test
end end
def test_nil_value_does_not_raise 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) result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count) assert_equal(0, t.errors.count)
+5 -74
View File
@@ -176,102 +176,33 @@ class VariableTest < Minitest::Test
) )
end 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 def test_filter_with_single_trailing_comma
template = '{{ "hello" | append: "world", }}' template = '{{ "hello" | append: "world", }}'
with_error_modes(:strict) do assert_template_result('helloworld', template)
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
end end
def test_multiple_filters_with_trailing_commas def test_multiple_filters_with_trailing_commas
template = '{{ "hello" | append: "1", | append: "2", }}' template = '{{ "hello" | append: "1", | append: "2", }}'
with_error_modes(:strict) do assert_template_result('hello12', template)
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
end end
def test_filter_with_colon_but_no_arguments def test_filter_with_colon_but_no_arguments
template = '{{ "test" | upcase: }}' template = '{{ "test" | upcase: }}'
with_error_modes(:strict) do assert_template_result('TEST', template)
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
end end
def test_filter_chain_with_colon_no_args def test_filter_chain_with_colon_no_args
template = '{{ "test" | append: "x" | upcase: }}' template = '{{ "test" | append: "x" | upcase: }}'
with_error_modes(:strict) do assert_template_result('TESTX', template)
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
end end
def test_combining_trailing_comma_and_empty_args def test_combining_trailing_comma_and_empty_args
template = '{{ "test" | append: "x", | upcase: }}' template = '{{ "test" | append: "x", | upcase: }}'
with_error_modes(:strict) do assert_template_result('TESTX', template)
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
end end
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.rb'
require 'liquid/profiler' 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') if Minitest.const_defined?('Test')
# We're on Minitest 5+. Nothing to do here. # We're on Minitest 5+. Nothing to do here.
else else
@@ -34,27 +27,27 @@ module Minitest
def assert_template_result( def assert_template_result(
expected, template, assigns = {}, 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 template_factory: nil
) )
file_system = StubFileSystem.new(partials || {}) file_system = StubFileSystem.new(partials || {})
environment = Liquid::Environment.build(file_system: file_system) 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) 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) context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
output = template.render(context) output = template.render(context)
assert_equal(expected, output, message) assert_equal(expected, output, message)
end end
def assert_match_syntax_error(match, template, error_mode: nil) def assert_match_syntax_error(match, template)
exception = assert_raises(Liquid::SyntaxError) do 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 end
assert_match(match, exception.message) assert_match(match, exception.message)
end end
def assert_syntax_error(template, error_mode: nil) def assert_syntax_error(template)
assert_match_syntax_error("", template, error_mode: error_mode) assert_match_syntax_error("", template)
end end
def assert_usage_increment(name, times: 1) def assert_usage_increment(name, times: 1)
@@ -82,16 +75,6 @@ module Minitest
Environment.dangerously_override(environment, &blk) Environment.dangerously_override(environment, &blk)
end 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) def with_custom_tag(tag_name, tag_class, &block)
environment = Liquid::Environment.default.dup environment = Liquid::Environment.default.dup
environment.register_tag(tag_name, tag_class) 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 def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @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_false(array_expr, 'contains', 0)
assert_evaluates_true(array_expr, 'contains', 1) assert_evaluates_true(array_expr, 'contains', 1)
@@ -96,8 +96,8 @@ class ConditionUnitTest < Minitest::Test
def test_contains_returns_false_for_nil_operands def test_contains_returns_false_for_nil_operands
@context = Liquid::Context.new @context = Liquid::Context.new
assert_evaluates_false(VariableLookup.new('not_assigned'), 'contains', '0') assert_evaluates_false(VariableLookup.parse('not_assigned'), 'contains', '0')
assert_evaluates_false(0, 'contains', VariableLookup.new('not_assigned')) assert_evaluates_false(0, 'contains', VariableLookup.parse('not_assigned'))
end end
def test_contains_return_false_on_wrong_data_type def test_contains_return_false_on_wrong_data_type
@@ -149,7 +149,7 @@ class ConditionUnitTest < Minitest::Test
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @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 end
def test_default_context_is_deprecated def test_default_context_is_deprecated
@@ -166,35 +166,24 @@ class ConditionUnitTest < Minitest::Test
assert_includes(err.lines.map(&:strip), expected) assert_includes(err.lines.map(&:strip), expected)
end end
def test_parse_expression_in_strict_mode def test_parse_expression
environment = Environment.build(error_mode: :strict) environment = Environment.build
parse_context = ParseContext.new(environment: environment) 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_instance_of(VariableLookup, result)
assert_equal('product', result.name) assert_equal('product', result.name)
assert_equal(['title'], result.lookups) assert_equal(['title'], result.lookups)
end end
def test_parse_expression_in_strict2_mode_raises_internal_error def test_parse_expression_returns_method_literal_for_blank_and_empty
environment = Environment.build(error_mode: :strict2) environment = Environment.build
parse_context = ParseContext.new(environment: environment) parse_context = ParseContext.new(environment: environment)
parser = parse_context.new_parser('blank')
result = Condition.parse_expression(parser)
error = assert_raises(Liquid::InternalError) do assert_instance_of(Condition::MethodLiteral, result)
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)
end end
# Tests for blank? comparison without ActiveSupport # Tests for blank? comparison without ActiveSupport
@@ -216,7 +205,7 @@ class ConditionUnitTest < Minitest::Test
@context['whitespace'] = ' ' @context['whitespace'] = ' '
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_blank_with_empty_string def test_blank_with_empty_string
@@ -225,7 +214,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_string'] = '' @context['empty_string'] = ''
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_blank_with_empty_array def test_blank_with_empty_array
@@ -234,7 +223,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_array'] = [] @context['empty_array'] = []
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_blank_with_empty_hash def test_blank_with_empty_hash
@@ -243,7 +232,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_hash'] = {} @context['empty_hash'] = {}
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_blank_with_nil def test_blank_with_nil
@@ -252,7 +241,7 @@ class ConditionUnitTest < Minitest::Test
@context['nil_value'] = nil @context['nil_value'] = nil
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_blank_with_false def test_blank_with_false
@@ -261,7 +250,7 @@ class ConditionUnitTest < Minitest::Test
@context['false_value'] = false @context['false_value'] = false
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_not_blank_with_true def test_not_blank_with_true
@@ -270,7 +259,7 @@ class ConditionUnitTest < Minitest::Test
@context['true_value'] = true @context['true_value'] = true
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_not_blank_with_number def test_not_blank_with_number
@@ -279,7 +268,7 @@ class ConditionUnitTest < Minitest::Test
@context['number'] = 42 @context['number'] = 42
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_not_blank_with_string_content def test_not_blank_with_string_content
@@ -288,7 +277,7 @@ class ConditionUnitTest < Minitest::Test
@context['string'] = 'hello' @context['string'] = 'hello'
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_not_blank_with_non_empty_array def test_not_blank_with_non_empty_array
@@ -297,7 +286,7 @@ class ConditionUnitTest < Minitest::Test
@context['array'] = [1, 2, 3] @context['array'] = [1, 2, 3]
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
def test_not_blank_with_non_empty_hash def test_not_blank_with_non_empty_hash
@@ -306,7 +295,7 @@ class ConditionUnitTest < Minitest::Test
@context['hash'] = { 'a' => 1 } @context['hash'] = { 'a' => 1 }
blank_literal = Condition.class_variable_get(:@@method_literals)['blank'] 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 end
# Tests for empty? comparison without ActiveSupport # Tests for empty? comparison without ActiveSupport
@@ -322,7 +311,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_string'] = '' @context['empty_string'] = ''
empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] 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 end
def test_empty_with_whitespace_string_not_empty def test_empty_with_whitespace_string_not_empty
@@ -332,7 +321,7 @@ class ConditionUnitTest < Minitest::Test
@context['whitespace'] = ' ' @context['whitespace'] = ' '
empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] 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 end
def test_empty_with_empty_array def test_empty_with_empty_array
@@ -341,7 +330,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_array'] = [] @context['empty_array'] = []
empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] 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 end
def test_empty_with_empty_hash def test_empty_with_empty_hash
@@ -350,7 +339,7 @@ class ConditionUnitTest < Minitest::Test
@context['empty_hash'] = {} @context['empty_hash'] = {}
empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] 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 end
def test_nil_is_not_empty def test_nil_is_not_empty
@@ -360,7 +349,7 @@ class ConditionUnitTest < Minitest::Test
@context['nil_value'] = nil @context['nil_value'] = nil
empty_literal = Condition.class_variable_get(:@@method_literals)['empty'] 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 end
private private
+24 -87
View File
@@ -5,119 +5,56 @@ require 'test_helper'
class ParseContextUnitTest < Minitest::Test class ParseContextUnitTest < Minitest::Test
include Liquid include Liquid
def test_safe_parse_expression_with_variable_lookup def test_parser_expression_with_variable_lookup
parser_strict = strict_parse_context.new_parser('product.title') parser = parse_context.new_parser('product.title')
result_strict = strict_parse_context.safe_parse_expression(parser_strict) result = parser.expression
parser_strict2 = strict2_parse_context.new_parser('product.title') assert_instance_of(VariableLookup, result)
result_strict2 = strict2_parse_context.safe_parse_expression(parser_strict2) assert_equal('product', result.name)
assert_equal(['title'], result.lookups)
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)
end end
def test_safe_parse_expression_raises_syntax_error_for_invalid_expression def test_parser_expression_raises_syntax_error_for_invalid_expression
parser_strict = strict_parse_context.new_parser('') parser = parse_context.new_parser('')
parser_strict2 = strict2_parse_context.new_parser('')
error_strict = assert_raises(Liquid::SyntaxError) do error = assert_raises(Liquid::SyntaxError) do
strict_parse_context.safe_parse_expression(parser_strict) parser.expression
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)
end end
assert_match(/is not a valid expression/, error_strict2.message) assert_match(/is not a valid expression/, error.message)
end end
def test_parse_expression_with_variable_lookup 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_instance_of(VariableLookup, result)
assert_equal('product', result_strict.name) assert_equal('product', result.name)
assert_equal(['title'], result_strict.lookups) assert_equal(['title'], result.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)
end end
def test_parse_expression_with_safe_true def test_parser_expression_advances_parser_pointer
result_strict = strict_parse_context.parse_expression('product.title', safe: true) parser = parse_context.new_parser('foo, bar')
assert_instance_of(VariableLookup, result_strict) # parser.expression consumes "foo"
assert_equal('product', result_strict.name) first_result = parser.expression
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)
assert_instance_of(VariableLookup, first_result) assert_instance_of(VariableLookup, first_result)
assert_equal('foo', first_result.name) assert_equal('foo', first_result.name)
parser.consume(:comma) parser.consume(:comma)
# safe_parse_expression consumes "bar" # parser.expression consumes "bar"
second_result = strict2_parse_context.safe_parse_expression(parser) second_result = parser.expression
assert_instance_of(VariableLookup, second_result) assert_instance_of(VariableLookup, second_result)
assert_equal('bar', second_result.name) assert_equal('bar', second_result.name)
parser.consume(:end_of_string) parser.consume(:end_of_string)
end end
def test_parse_expression_with_whitespace_in_strict2_mode
result = strict2_parse_context.parse_expression(' ', safe: true)
assert_nil(result)
end
private private
def strict_parse_context def parse_context
@strict_parse_context ||= ParseContext.new( @parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :strict), environment: Environment.build,
)
end
def strict2_parse_context
@strict2_parse_context ||= ParseContext.new(
environment: Environment.build(error_mode: :strict2),
) )
end end
end end
+69 -16
View File
@@ -45,40 +45,93 @@ class ParserUnitTest < Minitest::Test
assert_equal(false, p.look(:number, 1)) assert_equal(false, p.look(:number, 1))
end end
def test_expressions def test_expression_string
p = new_parser("hi.there hi?[5].there? hi.there.bob") p = new_parser("hi.there hi?[5].there? hi.there.bob")
assert_equal('hi.there', p.expression) assert_equal('hi.there', p.expression_string)
assert_equal('hi?[5].there?', p.expression) assert_equal('hi?[5].there?', p.expression_string)
assert_equal('hi.there.bob', p.expression) assert_equal('hi.there.bob', p.expression_string)
p = new_parser("567 6.0 'lol' \"wut\"") p = new_parser("567 6.0 'lol' \"wut\"")
assert_equal('567', p.expression) assert_equal('567', p.expression_string)
assert_equal('6.0', p.expression) assert_equal('6.0', p.expression_string)
assert_equal("'lol'", p.expression) assert_equal("'lol'", p.expression_string)
assert_equal('"wut"', p.expression) 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 end
def test_ranges def test_ranges
p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)") p = new_parser("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
assert_equal('(5..7)', p.expression) assert_equal('(5..7)', p.expression_string)
assert_equal('(1.5..9.6)', p.expression) assert_equal('(1.5..9.6)', p.expression_string)
assert_equal('(young..old)', p.expression) assert_equal('(young..old)', p.expression_string)
assert_equal('(hi[5].wat..old)', p.expression) assert_equal('(hi[5].wat..old)', p.expression_string)
end end
def test_arguments def test_argument_string
p = new_parser("filter: hi.there[5], keyarg: 7") p = new_parser("filter: hi.there[5], keyarg: 7")
assert_equal('filter', p.consume(:id)) assert_equal('filter', p.consume(:id))
assert_equal(':', p.consume(:colon)) 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(',', p.consume(:comma))
assert_equal('keyarg: 7', p.argument) assert_equal('keyarg: 7', p.argument_string)
end end
def test_invalid_expression def test_invalid_expression
assert_raises(SyntaxError) do assert_raises(SyntaxError) do
p = new_parser("==") p = new_parser("==")
p.expression p.expression_string
end end
end end
+7 -9
View File
@@ -175,7 +175,7 @@ class PartialCacheUnitTest < Minitest::Test
assert_equal('some/path/my_partial', partial.name) assert_equal('some/path/my_partial', partial.name)
end end
def test_includes_error_mode_into_template_cache def test_cache_key
template_factory = StubTemplateFactory.new template_factory = StubTemplateFactory.new
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { registers: {
@@ -184,16 +184,14 @@ class PartialCacheUnitTest < Minitest::Test
}, },
) )
[:lax, :warn, :strict, :strict2].each do |error_mode| Liquid::PartialCache.load(
Liquid::PartialCache.load( 'my_partial',
'my_partial', context: context,
context: context, parse_context: Liquid::ParseContext.new,
parse_context: Liquid::ParseContext.new(error_mode: error_mode), )
)
end
assert_equal( assert_equal(
["my_partial:lax", "my_partial:warn", "my_partial:strict", "my_partial:strict2"], ["my_partial"],
context.registers[:cached_partials].keys, context.registers[:cached_partials].keys,
) )
end end
+16 -46
View File
@@ -20,15 +20,9 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%} {%- endcase -%}
LIQUID LIQUID
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_template_result("one", template)
end
with_error_modes(:strict2) do assert_match(/Expected end_of_string but found/, error.message)
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
end
end end
def test_case_when_with_trailing_element def test_case_when_with_trailing_element
@@ -41,15 +35,9 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%} {%- endcase -%}
LIQUID LIQUID
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_template_result("one", template)
end
with_error_modes(:strict2) do assert_match(/Expected end_of_string but found/, error.message)
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Expected end_of_string but found/, error.message)
end
end end
def test_case_when_with_comma def test_case_when_with_comma
@@ -62,9 +50,7 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%} {%- endcase -%}
LIQUID LIQUID
with_error_modes(:lax, :strict, :strict2) do assert_template_result("one", template)
assert_template_result("one", template)
end
end end
def test_case_when_with_or def test_case_when_with_or
@@ -77,9 +63,7 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%} {%- endcase -%}
LIQUID LIQUID
with_error_modes(:lax, :strict, :strict2) do assert_template_result("one", template)
assert_template_result("one", template)
end
end end
def test_case_when_empty def test_case_when_empty
@@ -92,14 +76,12 @@ class CaseTagUnitTest < Minitest::Test
{%- endcase -%} {%- endcase -%}
LIQUID 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' => 2 }) assert_template_result("2 or empty", template, { 'x' => {} })
assert_template_result("2 or empty", template, { 'x' => {} }) 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' => 'b' } }) assert_template_result("not 2 or empty", template, { 'x' => ['a'] })
assert_template_result("not 2 or empty", template, { 'x' => ['a'] }) assert_template_result("not 2 or empty", template, { 'x' => 4 })
assert_template_result("not 2 or empty", template, { 'x' => 4 })
end
end end
def test_case_with_invalid_expression def test_case_with_invalid_expression
@@ -113,15 +95,9 @@ class CaseTagUnitTest < Minitest::Test
LIQUID LIQUID
assigns = { 'foo' => { 'bar' => 'baz' } } assigns = { 'foo' => { 'bar' => 'baz' } }
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
assert_template_result("one", template, assigns)
end
with_error_modes(:strict2) do assert_match(/Unexpected character =/, error.message)
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
def test_case_when_with_invalid_expression def test_case_when_with_invalid_expression
@@ -135,14 +111,8 @@ class CaseTagUnitTest < Minitest::Test
LIQUID LIQUID
assigns = { 'foo' => { 'bar' => 'baz' } } assigns = { 'foo' => { 'bar' => 'baz' } }
with_error_modes(:lax, :strict) do error = assert_raises(Liquid::SyntaxError) { Template.parse(template, assigns) }
assert_template_result("one", template, assigns)
end
with_error_modes(:strict2) do assert_match(/Unexpected character =/, error.message)
error = assert_raises(Liquid::SyntaxError) { Template.parse(template) }
assert_match(/Unexpected character =/, error.message)
end
end end
end end
+40 -80
View File
@@ -7,20 +7,20 @@ class VariableUnitTest < Minitest::Test
def test_variable def test_variable
var = create_variable('hello') var = create_variable('hello')
assert_equal(VariableLookup.new('hello'), var.name) assert_equal(VariableLookup.parse('hello'), var.name)
end end
def test_filters def test_filters
var = create_variable('hello | textileze') var = create_variable('hello | textileze')
assert_equal(VariableLookup.new('hello'), var.name) assert_equal(VariableLookup.parse('hello'), var.name)
assert_equal([['textileze', []]], var.filters) assert_equal([['textileze', []]], var.filters)
var = create_variable('hello | textileze | paragraph') 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) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable(%( hello | strftime: '%Y')) 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) assert_equal([['strftime', ['%Y']]], var.filters)
var = create_variable(%( 'typo' | link_to: 'Typo', true )) var = create_variable(%( 'typo' | link_to: 'Typo', true ))
@@ -44,11 +44,11 @@ class VariableUnitTest < Minitest::Test
assert_equal([['repeat', [3, 3, 3]]], var.filters) assert_equal([['repeat', [3, 3, 3]]], var.filters)
var = create_variable(%( hello | strftime: '%Y, okay?')) 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) assert_equal([['strftime', ['%Y, okay?']]], var.filters)
var = create_variable(%( hello | things: "%Y, okay?", 'the other one')) 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) assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters)
end end
@@ -60,24 +60,18 @@ class VariableUnitTest < Minitest::Test
def test_filters_without_whitespace def test_filters_without_whitespace
var = create_variable('hello | textileze | paragraph') 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) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable('hello|textileze|paragraph') 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) assert_equal([['textileze', []], ['paragraph', []]], var.filters)
var = create_variable("hello|replace:'foo','bar'|textileze") 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) assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters)
end 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 def test_string_to_filter
var = create_variable("'http://disney.com/logo.gif' | image: 'med' ") var = create_variable("'http://disney.com/logo.gif' | image: 'med' ")
assert_equal('http://disney.com/logo.gif', var.name) assert_equal('http://disney.com/logo.gif', var.name)
@@ -105,14 +99,12 @@ class VariableUnitTest < Minitest::Test
end end
def test_dashes def test_dashes
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name) assert_equal(VariableLookup.parse('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-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 - bar') } assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') } assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
end
end end
def test_string_with_special_chars def test_string_with_special_chars
@@ -122,79 +114,47 @@ class VariableUnitTest < Minitest::Test
def test_string_dot def test_string_dot
var = create_variable(%( test.test )) var = create_variable(%( test.test ))
assert_equal(VariableLookup.new('test.test'), var.name) assert_equal(VariableLookup.parse('test.test'), var.name)
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye')) 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) assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters)
end end
def test_lax_filter_argument_parsing def test_filter_argument_parsing
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax) # optional colon
assert_equal(VariableLookup.new('number_of_comments'), var.name) var = create_variable(%(n | f1 | f2:))
assert_equal([['pluralize', ['comment', 'comments']]], var.filters) assert_equal([['f1', []], ['f2', []]], var.filters)
# missing does not throws error # missing argument throws error
create_variable(%(n | f1: ,), error_mode: :lax) assert_raises(SyntaxError) { create_variable(%(n | f1: ,)) }
create_variable(%(n | f1: ,| f2), error_mode: :lax) 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 ':' # arg requires colon
var = create_variable(%(n | f1 1 | f2 k1: v1), error_mode: :lax) assert_raises(SyntaxError) { create_variable(%(n | f1 1)) }
assert_equal([['f1', []], ['f2', [VariableLookup.new('v1')]]], var.filters)
# 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 # 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) 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) # 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) assert_equal([['link_to', ["https://example.com"], { "class" => "black", "title" => "title" }]], var.filters)
end
def test_strict_filter_argument_parsing # string key throws
with_error_modes(:strict) do assert_raises(SyntaxError) { create_variable(%(n | pluralize: 'comment': 'comments')) }
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
end end
def test_output_raw_source_of_variable def test_output_raw_source_of_variable
@@ -203,7 +163,7 @@ class VariableUnitTest < Minitest::Test
end end
def test_variable_lookup_interface def test_variable_lookup_interface
lookup = VariableLookup.new('a.b.c') lookup = VariableLookup.parse('a.b.c')
assert_equal('a', lookup.name) assert_equal('a', lookup.name)
assert_equal(['b', 'c'], lookup.lookups) assert_equal(['b', 'c'], lookup.lookups)
end end