Compare commits

...

27 Commits

Author SHA1 Message Date
Juan Broullon
1707980a48 Fix stack level too deep error 2017-05-09 11:54:20 -04:00
Florian Weingarten
1370a102c9 Merge pull request #789 from evulse/contains-strict-fix
Allow variables to start with contains in strict parser
2017-03-24 09:50:31 -04:00
Mike Angell
c9bac9befe Merge branch 'master' into contains-strict-fix 2017-03-24 11:09:09 +10:00
Mike
210a0616f3 Update History to include fix 2017-03-24 10:35:56 +10:00
Lasse Skindstad Ebert
5149cde5c3 Fix include tag used with strict_variables (#829)
Fixes https://github.com/Shopify/liquid/issues/828
2017-03-22 16:00:31 -04:00
Florian Weingarten
22f2cec5de Merge pull request #864 from chenxianyu2015/fix-strainer-add_filter-method
fix  #861: duplicate inclusion condition logic error of Liquid::Strainer.add_filter method
2017-02-23 14:06:20 -05:00
chenxianyu
4318240ae0 test: modify Strainer.add_filter duplicate inclusion test case 2017-02-22 10:33:22 +08:00
chenxianyu
aa79c33dda fix: Strainer.add_filter method 2017-02-13 15:50:19 +08:00
Justin Li
b1ef28566e Merge pull request #846 from mrmanc/master
Clarifies spelling of for’s reversed flag to address #843
2017-02-10 19:26:38 -05:00
Justin Li
41bcc48222 Merge pull request #854 from jaredbeck/patch-1
Docs: Help people upgrade to 4, re: liquid_methods
2017-02-10 19:25:04 -05:00
Dylan Thacker-Smith
27d5106dc9 Merge pull request #860 from Shopify/handle-string-node-render-exc
Avoid calling line_number on String node when rescuing a render error.
2017-02-10 14:13:11 -05:00
Dylan Thacker-Smith
7334073be2 Avoid duck typing to detect whether to call render on a node. 2017-02-10 13:49:26 -05:00
Dylan Thacker-Smith
5dcefd7d77 Avoid calling line_number on String node when rescuing a render error. 2017-02-07 15:34:10 -05:00
Richard Monette
25c7b05916 Merge pull request #857 from Shopify/handle-join-on-fixnum
handle join on fixnum
2017-02-01 14:25:40 -05:00
Richard Monette
d17f86ba4d handle join on fixnum 2017-02-01 12:47:35 -05:00
Jerry Liu
384e4313ff Merge pull request #851 from Shopify/benchmark-render
Allow benchmarks to benchmark render by itself
2017-01-31 17:18:56 -05:00
Jerry Liu
23f2af8ff5 fix travis build 2017-01-31 17:04:36 -05:00
Jerry Liu
a93eac0268 Introduce new benchmarking methods to liquid to use on rubybench 2017-01-27 10:56:16 -05:00
Florian Weingarten
2cc7493cb0 Merge pull request #855 from Shopify/bundler-benchmark-group
Create a benchmark group in Gemfile
2017-01-20 16:41:11 -05:00
Jerry Liu
85463e1753 add benchmark-ips to benchmark group in Gemfile 2017-01-20 16:04:42 -05:00
Jared Beck
52ff9b0e84 Docs: Help people upgrade to 4, re: liquid_methods
The discussion in #568 helped me.

[ci skip]
2017-01-19 14:23:39 -05:00
Dylan Thacker-Smith
0c58328a40 test: Equality comparison of two hashes (#850) 2017-01-16 15:56:38 -05:00
Dylan Thacker-Smith
2bb3552033 Fix internal liquid error when comparing hash with incompatible type (#849) 2017-01-16 13:13:17 -05:00
Mark Crossfield
8b751ddf46 Removes a non ascii character from comment to appease Rubocop 2017-01-09 10:16:35 +00:00
Mark Crossfield
e5cbdb2b27 Clarifies spelling of for’s reversed flag to address #843
It should now be harder to read the docs and miss the extra letter required for reversed compared to reverse, which causes a fairly generic syntax warning when trying to reverse sort a collection in a for loop.
2017-01-08 12:44:12 +00:00
Michael Angell
6ed6e7e12f Allow :id to start with the word contains 2016-08-20 20:32:46 +10:00
Mike Angell
f41ed78378 Merge pull request #1 from Shopify/master
Pull inline with upstream
2016-08-17 21:30:08 +10:00
19 changed files with 160 additions and 47 deletions

View File

@@ -19,6 +19,10 @@ matrix:
allow_failures: allow_failures:
- rvm: jruby-head - rvm: jruby-head
install:
- gem install rainbow -v 2.2.1
- bundle install
script: "bundle exec rake" script: "bundle exec rake"
notifications: notifications:

View File

@@ -3,9 +3,12 @@ source 'https://rubygems.org'
gemspec gemspec
gem 'stackprof', platforms: :mri_21 gem 'stackprof', platforms: :mri_21
group :benchmark, :test do
gem 'benchmark-ips'
end
group :test do group :test do
gem 'spy', '0.4.1' gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop', '0.34.2' gem 'rubocop', '0.34.2'
platform :mri do platform :mri do

View File

@@ -20,10 +20,13 @@
* Add concat filter to concatenate arrays (#429) [Diogo Beato] * Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li] * Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith] * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove support for `liquid_methods` * Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande] * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed ### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette] * Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette] * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo] * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]

View File

@@ -93,10 +93,11 @@ module Liquid
rescue MemoryError => e rescue MemoryError => e
raise e raise e
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, token.line_number, token.raw) context.handle_error(e, token.line_number)
output << nil output << nil
rescue ::StandardError => e rescue ::StandardError => e
output << context.handle_error(e, token.line_number, token.raw) line_number = token.is_a?(String) ? nil : token.line_number
output << context.handle_error(e, line_number)
end end
end end
@@ -106,7 +107,7 @@ module Liquid
private private
def render_node(node, context) def render_node(node, context)
node_output = (node.respond_to?(:render) ? node.render(context) : node) node_output = node.is_a?(String) ? node : node.render(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += node_output.length context.resource_limits.render_length += node_output.length

View File

@@ -7,6 +7,7 @@ module Liquid
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition #:nodoc:
@@depth = 0
@@operators = { @@operators = {
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -47,6 +48,11 @@ module Liquid
when :or when :or
result || @child_condition.evaluate(context) result || @child_condition.evaluate(context)
when :and when :and
@@depth += 1
if @@depth >= 500
@@depth = 0
raise StackLevelError, "Nesting too deep".freeze
end
result && @child_condition.evaluate(context) result && @child_condition.evaluate(context)
else else
result result
@@ -110,7 +116,7 @@ module Liquid
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
elsif left.respond_to?(operation) && right.respond_to?(operation) elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e

View File

@@ -74,7 +74,7 @@ module Liquid
@interrupts.pop @interrupts.pop
end end
def handle_error(e, line_number = nil, raw_token = nil) def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
@@ -160,7 +160,7 @@ module Liquid
end end
# Fetches an object starting at the local scope and then moving up the hierachy # Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key) def find_variable(key, raise_on_not_found: true)
# This was changed from find() to find_index() because this is a very hot # This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation # path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) } index = @scopes.find_index { |s| s.key?(key) }
@@ -170,7 +170,7 @@ module Liquid
if scope.nil? if scope.nil?
@environments.each do |e| @environments.each do |e|
variable = lookup_and_evaluate(e, key) variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
unless variable.nil? unless variable.nil?
scope = e scope = e
break break
@@ -179,7 +179,7 @@ module Liquid
end end
scope ||= @environments.last || @scopes.last scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key) variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
@@ -187,8 +187,8 @@ module Liquid
variable variable
end end
def lookup_and_evaluate(obj, key) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key) if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}" raise Liquid::UndefinedVariable, "undefined variable #{key}"
end end

View File

@@ -18,10 +18,10 @@ module Liquid
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
def initialize(input) def initialize(input)
@ss = StringScanner.new(input.rstrip) @ss = StringScanner.new(input)
end end
def tokenize def tokenize
@@ -29,6 +29,7 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(/\s*/) @ss.skip(/\s*/)
break if @ss.eos?
tok = case tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]

View File

@@ -384,7 +384,7 @@ module Liquid
end end
def join(glue) def join(glue)
to_a.join(glue) to_a.join(glue.to_s)
end end
def concat(args) def concat(args)

View File

@@ -27,7 +27,7 @@ module Liquid
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.class.include?(filter) unless self.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any? if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"

View File

@@ -23,7 +23,7 @@ module Liquid
# {{ item.name }} # {{ item.name }}
# {% end %} # {% end %}
# #
# To reverse the for loop simply use {% for item in collection reversed %} # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
# #
# == Available variables: # == Available variables:
# #

View File

@@ -50,7 +50,7 @@ module Liquid
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
else else
context.find_variable(template_name) context.find_variable(template_name, raise_on_not_found: false)
end end
old_template_name = context.template_name old_template_name = context.template_name

View File

@@ -5,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 60 x.time = 10
x.warmup = 5 x.warmup = 5
puts puts
@@ -13,5 +13,6 @@ Benchmark.ips do |x|
puts puts
x.report("parse:") { profiler.compile } x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run } x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end end

View File

@@ -21,53 +21,100 @@ class ThemeRunner
end end
end end
# Load all templates into memory, do this now so that # Initialize a new liquid ThemeRunner instance
# we don't profile IO. # Will load all templates into memory, do this now so that we don't profile IO.
def initialize def initialize
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid' next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid' theme_path = File.dirname(test) + '/theme.liquid'
{
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test] liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
}
end.compact end.compact
compile_all_tests
end end
# `compile` will test just the compilation portion of liquid without any templates
def compile def compile
# Dup assigns because will make some changes to them @tests.each do |test_hash|
Liquid::Template.new.parse(test_hash[:liquid])
@tests.each do |liquid, layout, template_name| Liquid::Template.new.parse(test_hash[:layout])
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
end end
end end
# `run` is called to benchmark rendering and compiling at the same time
def run def run
# Dup assigns because will make some changes to them each_test do |liquid, layout, assigns, page_template, template_name|
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name) compile_and_render(liquid, layout, assigns, page_template, template_name)
end end
end end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
end
end
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
end end
end end

View File

@@ -115,4 +115,8 @@ class ParsingQuirksTest < Minitest::Test
assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}") assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}")
end end
end end
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end
end # ParsingQuirksTest end # ParsingQuirksTest

View File

@@ -170,6 +170,7 @@ class StandardFiltersTest < Minitest::Test
def test_join def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4]) assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ') assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
end end
def test_sort def test_sort

View File

@@ -235,4 +235,11 @@ class IncludeTagTest < Minitest::Test
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }
end end
def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true)
assert_equal [], template.errors
end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -64,6 +64,14 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_argument_error '1', '<=', 0 assert_evaluates_argument_error '1', '<=', 0
end end
def test_hash_compare_backwards_compatibility
assert_equal nil, Condition.new({}, '>', 2).evaluate
assert_equal nil, Condition.new(2, '>', {}).evaluate
assert_equal false, Condition.new({}, '==', 2).evaluate
assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate
assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate
end
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]
@@ -122,6 +130,17 @@ class ConditionUnitTest < Minitest::Test
assert_equal false, condition.evaluate assert_equal false, condition.evaluate
end end
def test_maximum_recursion_depth
condition = Condition.new(1, '==', 1)
assert_raises(Liquid::StackLevelError) do
(1..510).each do
condition.evaluate
condition.and Condition.new(2, '==', 2)
end
end
end
def test_should_allow_custom_proc_operator def test_should_allow_custom_proc_operator
Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} } Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }

View File

@@ -19,7 +19,7 @@ class LexerUnitTest < Minitest::Test
end end
def test_comparison def test_comparison
tokens = Lexer.new('== <> contains').tokenize tokens = Lexer.new('== <> contains ').tokenize
assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens assert_equal [[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens
end end

View File

@@ -145,4 +145,20 @@ class StrainerUnitTest < Minitest::Test
Strainer.global_filter(LateAddedFilter) Strainer.global_filter(LateAddedFilter)
assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input') assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input')
end end
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal 1, mod.include_count
end
end # StrainerTest end # StrainerTest