liquid 4.0.0.rc1 → 4.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8cb85d26c5a294b870e0624188f36b3a7e8c8ba7
4
- data.tar.gz: 1eea59fdf71a08c68a61dc240bf6655e244c7777
3
+ metadata.gz: 7a15813568dcc19866675ad50444b7939b5eecfa
4
+ data.tar.gz: acfb1e49596e266170a25c584978ec8ade8bcf01
5
5
  SHA512:
6
- metadata.gz: ce2003217fb7c25700c641d5af17b6b41e5247f062dba6ae34869abde88d169fd1407b210ce00f03465742a105294e202b97b98698a95a2992cdeaad66cd8429
7
- data.tar.gz: 9ff0d24f24b9c9411ddfa3b74577e6945dfc2b69f01508c9da6d409e82bc20629f0448110b3ee4e2f35c3e4c307c0c1e75e656659f3c78bb8fdbfd2b168a9831
6
+ metadata.gz: 20fa5f4ceec720a9c57219f71bb848c7b407529cf46bf7353fba196452bf9945e33d79e25aff8fb92aceb32ac0f6d2299e6204e7fc5d6eec04134224dae725ef
7
+ data.tar.gz: 4ed81d181edfaf21bd25b303ec4e5085c9e734e3fb9679aa3b7a88f664bced5ab60825bb163ffaf7498b650cedf779901ce78917abad1fcbc5be1ec7b27573ee
data/History.md CHANGED
@@ -3,6 +3,10 @@
3
3
  ## 4.0.0 / not yet released / branch "master"
4
4
 
5
5
  ### Changed
6
+ * Add to_number Drop method to allow custom drops to work with number filters (#731)
7
+ * Add strict_variables and strict_filters options to detect undefined references (#691)
8
+ * Improve loop performance (#681) [Florian Weingarten]
9
+ * Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
6
10
  * Add url_decode filter to invert url_encode (#645) [Larry Archer]
7
11
  * Add global_filter to apply a filter to all output (#610) [Loren Hale]
8
12
  * Add compact filter (#600) [Carson Reinke]
@@ -15,9 +19,12 @@
15
19
  * Ruby 1.9 support dropped (#491) [Justin Li]
16
20
  * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
17
21
  * Remove support for `liquid_methods`
18
- * Rename Drop method `before_method` as `liquid_method_missing` (#661) [Thierry Joyal]
22
+ * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
19
23
 
20
24
  ### Fixed
25
+ * Fix map filter when value is a Proc (#672) [Guillaume Malette]
26
+ * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
27
+ * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
21
28
  * Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
22
29
  * Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
23
30
  * Fix bug in uniq filter (#595) [Florian Weingarten]
data/README.md CHANGED
@@ -73,3 +73,34 @@ This is useful for doing things like enabling strict mode only in the theme edit
73
73
 
74
74
  It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
75
75
  It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
76
+
77
+ ### Undefined variables and filters
78
+
79
+ 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.
80
+ You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
81
+ When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
82
+ Here are some examples:
83
+
84
+ ```ruby
85
+ template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
86
+ template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
87
+ #=> '1 2 ' # when a variable is undefined, it's rendered as nil
88
+ template.errors
89
+ #=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
90
+ ```
91
+
92
+ ```ruby
93
+ template = Liquid::Template.parse("{{x | filter1 | upcase}}")
94
+ template.render({ 'x' => 'foo' }, { strict_filters: true })
95
+ #=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
96
+ template.errors
97
+ #=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
98
+ ```
99
+
100
+ If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
101
+
102
+ ```ruby
103
+ template = Liquid::Template.parse("{{x}} {{y}}")
104
+ template.render!({ 'x' => 1}, { strict_variables: true })
105
+ #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
106
+ ```
@@ -76,6 +76,9 @@ module Liquid
76
76
  end
77
77
  rescue MemoryError => e
78
78
  raise e
79
+ rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
80
+ context.handle_error(e, token.line_number)
81
+ output << nil
79
82
  rescue ::StandardError => e
80
83
  output << context.handle_error(e, token.line_number)
81
84
  end
@@ -13,7 +13,7 @@ module Liquid
13
13
  # context['bob'] #=> nil class Context
14
14
  class Context
15
15
  attr_reader :scopes, :errors, :registers, :environments, :resource_limits
16
- attr_accessor :exception_handler, :template_name, :partial, :global_filter
16
+ attr_accessor :exception_handler, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
17
17
 
18
18
  def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
19
19
  @environments = [environments].flatten
@@ -21,6 +21,7 @@ module Liquid
21
21
  @registers = registers
22
22
  @errors = []
23
23
  @partial = false
24
+ @strict_variables = false
24
25
  @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
25
26
  squash_instance_assigns_with_environments
26
27
 
@@ -205,7 +206,13 @@ module Liquid
205
206
  end
206
207
 
207
208
  def lookup_and_evaluate(obj, key)
208
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
209
+ if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
210
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
211
+ end
212
+
213
+ value = obj[key]
214
+
215
+ if value.is_a?(Proc) && obj.respond_to?(:[]=)
209
216
  obj[key] = (value.arity == 0) ? value.call : value.call(self)
210
217
  else
211
218
  value
data/lib/liquid/drop.rb CHANGED
@@ -24,8 +24,9 @@ module Liquid
24
24
  attr_writer :context
25
25
 
26
26
  # Catch all for the method
27
- def liquid_method_missing(_method)
28
- nil
27
+ def liquid_method_missing(method)
28
+ return nil unless @context && @context.strict_variables
29
+ raise Liquid::UndefinedDropMethod, "undefined method #{method}"
29
30
  end
30
31
 
31
32
  # called by liquid to invoke a drop
@@ -61,16 +62,17 @@ module Liquid
61
62
  end
62
63
 
63
64
  def self.invokable_methods
64
- unless @invokable_methods
65
+ @invokable_methods ||= begin
65
66
  blacklist = Liquid::Drop.public_instance_methods + [:each]
67
+
66
68
  if include?(Enumerable)
67
69
  blacklist += Enumerable.public_instance_methods
68
70
  blacklist -= [:sort, :count, :first, :min, :max, :include?]
69
71
  end
72
+
70
73
  whitelist = [:to_liquid] + (public_instance_methods - blacklist)
71
- @invokable_methods = Set.new(whitelist.map(&:to_s))
74
+ Set.new(whitelist.map(&:to_s))
72
75
  end
73
- @invokable_methods
74
76
  end
75
77
  end
76
78
  end
data/lib/liquid/errors.rb CHANGED
@@ -56,4 +56,8 @@ module Liquid
56
56
  MemoryError = Class.new(Error)
57
57
  ZeroDivisionError = Class.new(Error)
58
58
  FloatDomainError = Class.new(Error)
59
+ UndefinedVariable = Class.new(Error)
60
+ UndefinedDropMethod = Class.new(Error)
61
+ UndefinedFilter = Class.new(Error)
62
+ MethodOverrideError = Class.new(Error)
59
63
  end
@@ -65,7 +65,7 @@ module Liquid
65
65
  File.join(root, @pattern % template_path)
66
66
  end
67
67
 
68
- raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
68
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
69
69
 
70
70
  full_path
71
71
  end
@@ -1,7 +1,7 @@
1
1
  module Liquid
2
2
  class ParseContext
3
- attr_accessor :partial, :locale, :line_number
4
- attr_reader :warnings, :error_mode
3
+ attr_accessor :locale, :line_number
4
+ attr_reader :partial, :warnings, :error_mode
5
5
 
6
6
  def initialize(options = {})
7
7
  @template_options = options ? options.dup : {}
@@ -125,8 +125,6 @@ module Liquid
125
125
  []
126
126
  elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
127
127
  ary.sort { |a, b| a[property] <=> b[property] }
128
- elsif ary.first.respond_to?(property)
129
- ary.sort { |a, b| a.send(property) <=> b.send(property) }
130
128
  end
131
129
  end
132
130
 
@@ -141,8 +139,6 @@ module Liquid
141
139
  []
142
140
  elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
143
141
  ary.sort { |a, b| a[property].casecmp(b[property]) }
144
- elsif ary.first.respond_to?(property)
145
- ary.sort { |a, b| a.send(property).casecmp(b.send(property)) }
146
142
  end
147
143
  end
148
144
 
@@ -191,8 +187,6 @@ module Liquid
191
187
  []
192
188
  elsif ary.first.respond_to?(:[])
193
189
  ary.reject{ |a| a[property].nil? }
194
- elsif ary.first.respond_to?(property)
195
- ary.reject { |a| a.send(property).nil? }
196
190
  end
197
191
  end
198
192
 
@@ -222,6 +216,9 @@ module Liquid
222
216
  end
223
217
 
224
218
  def concat(input, array)
219
+ unless array.respond_to?(:to_ary)
220
+ raise ArgumentError.new("concat filter requires an array argument")
221
+ end
225
222
  InputIterator.new(input).concat(array)
226
223
  end
227
224
 
@@ -341,9 +338,12 @@ module Liquid
341
338
  raise Liquid::FloatDomainError, e.message
342
339
  end
343
340
 
344
- def default(input, default_value = "".freeze)
345
- is_blank = input.respond_to?(:empty?) ? input.empty? : !input
346
- is_blank ? default_value : input
341
+ def default(input, default_value = ''.freeze)
342
+ if !input || input.respond_to?(:empty?) && input.empty?
343
+ default_value
344
+ else
345
+ input
346
+ end
347
347
  end
348
348
 
349
349
  private
@@ -373,7 +373,7 @@ module Liquid
373
373
  end
374
374
 
375
375
  def concat(args)
376
- to_a.concat args
376
+ to_a.concat(args)
377
377
  end
378
378
 
379
379
  def reverse
@@ -26,10 +26,15 @@ module Liquid
26
26
  end
27
27
 
28
28
  def self.add_filter(filter)
29
- raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
29
+ raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
30
30
  unless self.class.include?(filter)
31
- send(:include, filter)
32
- @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
31
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
32
+ if invokable_non_public_methods.any?
33
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
34
+ else
35
+ send(:include, filter)
36
+ @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
37
+ end
33
38
  end
34
39
  end
35
40
 
@@ -48,6 +53,8 @@ module Liquid
48
53
  def invoke(method, *args)
49
54
  if self.class.invokable?(method)
50
55
  send(method, *args)
56
+ elsif @context && @context.strict_filters
57
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
51
58
  else
52
59
  args.first
53
60
  end
@@ -23,16 +23,28 @@ module Liquid
23
23
  def render(context)
24
24
  val = @from.render(context)
25
25
  context.scopes.last[@to] = val
26
-
27
- inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
28
- context.resource_limits.assign_score += inc
29
-
26
+ context.resource_limits.assign_score += assign_score_of(val)
30
27
  ''.freeze
31
28
  end
32
29
 
33
30
  def blank?
34
31
  true
35
32
  end
33
+
34
+ private
35
+
36
+ def assign_score_of(val)
37
+ if val.instance_of?(String)
38
+ val.length
39
+ elsif val.instance_of?(Array) || val.instance_of?(Hash)
40
+ sum = 1
41
+ # Uses #each to avoid extra allocations.
42
+ val.each { |child| sum += assign_score_of(child) }
43
+ sum
44
+ else
45
+ 1
46
+ end
47
+ end
36
48
  end
37
49
 
38
50
  Template.register_tag('assign'.freeze, Assign)
@@ -48,8 +48,10 @@ module Liquid
48
48
 
49
49
  def initialize(tag_name, markup, options)
50
50
  super
51
+ @from = @limit = nil
51
52
  parse_with_selected_parser(markup)
52
53
  @for_block = BlockBody.new
54
+ @else_block = nil
53
55
  end
54
56
 
55
57
  def parse(tokens)
@@ -20,7 +20,7 @@ module Liquid
20
20
 
21
21
  class TagRegistry
22
22
  def initialize
23
- @tags = {}
23
+ @tags = {}
24
24
  @cache = {}
25
25
  end
26
26
 
@@ -80,11 +80,11 @@ module Liquid
80
80
  end
81
81
 
82
82
  def error_mode
83
- @error_mode || :lax
83
+ @error_mode ||= :lax
84
84
  end
85
85
 
86
86
  def taint_mode
87
- @taint_mode || :lax
87
+ @taint_mode ||= :lax
88
88
  end
89
89
 
90
90
  # Pass a module with filter methods which should be available
@@ -107,6 +107,7 @@ module Liquid
107
107
  end
108
108
 
109
109
  def initialize
110
+ @rethrow_errors = false
110
111
  @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
111
112
  end
112
113
 
@@ -181,12 +182,7 @@ module Liquid
181
182
 
182
183
  registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
183
184
 
184
- context.add_filters(options[:filters]) if options[:filters]
185
-
186
- context.global_filter = options[:global_filter] if options[:global_filter]
187
-
188
- context.exception_handler = options[:exception_handler] if options[:exception_handler]
189
-
185
+ apply_options_to_context(context, options)
190
186
  when Module, Array
191
187
  context.add_filters(args.pop)
192
188
  end
@@ -235,5 +231,13 @@ module Liquid
235
231
  yield
236
232
  end
237
233
  end
234
+
235
+ def apply_options_to_context(context, options)
236
+ context.add_filters(options[:filters]) if options[:filters]
237
+ context.global_filter = options[:global_filter] if options[:global_filter]
238
+ context.exception_handler = options[:exception_handler] if options[:exception_handler]
239
+ context.strict_variables = options[:strict_variables] if options[:strict_variables]
240
+ context.strict_filters = options[:strict_filters] if options[:strict_filters]
241
+ end
238
242
  end
239
243
  end
@@ -4,7 +4,7 @@ module Liquid
4
4
 
5
5
  def initialize(source, line_numbers = false)
6
6
  @source = source
7
- @line_number = 1 if line_numbers
7
+ @line_number = line_numbers ? 1 : nil
8
8
  @tokens = tokenize
9
9
  end
10
10
 
data/lib/liquid/utils.rb CHANGED
@@ -50,9 +50,13 @@ module Liquid
50
50
  when Numeric
51
51
  obj
52
52
  when String
53
- (obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
53
+ (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
54
54
  else
55
- 0
55
+ if obj.respond_to?(:to_number)
56
+ obj.to_number
57
+ else
58
+ 0
59
+ end
56
60
  end
57
61
  end
58
62
 
@@ -55,9 +55,11 @@ module Liquid
55
55
  object = object.send(key).to_liquid
56
56
 
57
57
  # No key was present with the desired value and it wasn't one of the directly supported
58
- # keywords either. The only thing we got left is to return nil
58
+ # keywords either. The only thing we got left is to return nil or
59
+ # raise an exception if `strict_variables` option is set to true
59
60
  else
60
- return nil
61
+ return nil unless context.strict_variables
62
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
61
63
  end
62
64
 
63
65
  # If we are dealing with a drop here we have to
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Liquid
3
- VERSION = "4.0.0.rc1"
3
+ VERSION = "4.0.0.rc2"
4
4
  end
@@ -94,7 +94,7 @@ class ErrorHandlingTest < Minitest::Test
94
94
  )
95
95
  end
96
96
 
97
- assert_match /Liquid syntax error \(line 4\)/, err.message
97
+ assert_match(/Liquid syntax error \(line 4\)/, err.message)
98
98
  end
99
99
 
100
100
  def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
@@ -1,18 +1,18 @@
1
1
  require 'test_helper'
2
2
 
3
- module MoneyFilter
4
- def money(input)
5
- sprintf(' %d$ ', input)
3
+ class HashOrderingTest < Minitest::Test
4
+ module MoneyFilter
5
+ def money(input)
6
+ sprintf(' %d$ ', input)
7
+ end
6
8
  end
7
- end
8
9
 
9
- module CanadianMoneyFilter
10
- def money(input)
11
- sprintf(' %d$ CAD ', input)
10
+ module CanadianMoneyFilter
11
+ def money(input)
12
+ sprintf(' %d$ CAD ', input)
13
+ end
12
14
  end
13
- end
14
15
 
15
- class HashOrderingTest < Minitest::Test
16
16
  include Liquid
17
17
 
18
18
  def test_global_register_order
@@ -9,6 +9,10 @@ end
9
9
  class SecurityTest < Minitest::Test
10
10
  include Liquid
11
11
 
12
+ def setup
13
+ @assigns = {}
14
+ end
15
+
12
16
  def test_no_instance_eval
13
17
  text = %( {{ '1+1' | instance_eval }} )
14
18
  expected = %( 1+1 )
@@ -41,6 +41,16 @@ class TestEnumerable < Liquid::Drop
41
41
  end
42
42
  end
43
43
 
44
+ class NumberLikeThing < Liquid::Drop
45
+ def initialize(amount)
46
+ @amount = amount
47
+ end
48
+
49
+ def to_number
50
+ @amount
51
+ end
52
+ end
53
+
44
54
  class StandardFiltersTest < Minitest::Test
45
55
  include Liquid
46
56
 
@@ -364,20 +374,25 @@ class StandardFiltersTest < Minitest::Test
364
374
  def test_plus
365
375
  assert_template_result "2", "{{ 1 | plus:1 }}"
366
376
  assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
377
+
378
+ assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
367
379
  end
368
380
 
369
381
  def test_minus
370
382
  assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
371
383
  assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
384
+
385
+ assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
372
386
  end
373
387
 
374
388
  def test_times
375
389
  assert_template_result "12", "{{ 3 | times:4 }}"
376
390
  assert_template_result "0", "{{ 'foo' | times:4 }}"
377
-
378
391
  assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
379
-
380
392
  assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
393
+ assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
394
+ assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
395
+ assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
381
396
  end
382
397
 
383
398
  def test_divided_by
@@ -391,6 +406,8 @@ class StandardFiltersTest < Minitest::Test
391
406
  assert_raises(Liquid::ZeroDivisionError) do
392
407
  assert_template_result "4", "{{ 1 | modulo: 0 }}"
393
408
  end
409
+
410
+ assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
394
411
  end
395
412
 
396
413
  def test_modulo
@@ -398,6 +415,8 @@ class StandardFiltersTest < Minitest::Test
398
415
  assert_raises(Liquid::ZeroDivisionError) do
399
416
  assert_template_result "4", "{{ 1 | modulo: 0 }}"
400
417
  end
418
+
419
+ assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
401
420
  end
402
421
 
403
422
  def test_round
@@ -407,6 +426,9 @@ class StandardFiltersTest < Minitest::Test
407
426
  assert_raises(Liquid::FloatDomainError) do
408
427
  assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
409
428
  end
429
+
430
+ assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
431
+ assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
410
432
  end
411
433
 
412
434
  def test_ceil
@@ -415,6 +437,8 @@ class StandardFiltersTest < Minitest::Test
415
437
  assert_raises(Liquid::FloatDomainError) do
416
438
  assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
417
439
  end
440
+
441
+ assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
418
442
  end
419
443
 
420
444
  def test_floor
@@ -423,6 +447,8 @@ class StandardFiltersTest < Minitest::Test
423
447
  assert_raises(Liquid::FloatDomainError) do
424
448
  assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
425
449
  end
450
+
451
+ assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
426
452
  end
427
453
 
428
454
  def test_append
@@ -436,8 +462,7 @@ class StandardFiltersTest < Minitest::Test
436
462
  assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
437
463
  assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
438
464
 
439
- assert_raises(TypeError) do
440
- # no implicit conversion of Fixnum into Array
465
+ assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
441
466
  @filters.concat([1, 2], 10)
442
467
  end
443
468
  end
@@ -24,8 +24,8 @@ class RawTagTest < Minitest::Test
24
24
  end
25
25
 
26
26
  def test_invalid_raw
27
- assert_match_syntax_error /tag was never closed/, '{% raw %} foo'
28
- assert_match_syntax_error /Valid syntax/, '{% raw } foo {% endraw %}'
29
- assert_match_syntax_error /Valid syntax/, '{% raw } foo %}{% endraw %}'
27
+ assert_match_syntax_error(/tag was never closed/, '{% raw %} foo')
28
+ assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}')
29
+ assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}')
30
30
  end
31
31
  end
@@ -27,6 +27,12 @@ class ErroneousDrop < Liquid::Drop
27
27
  end
28
28
  end
29
29
 
30
+ class DropWithUndefinedMethod < Liquid::Drop
31
+ def foo
32
+ 'foo'
33
+ end
34
+ end
35
+
30
36
  class TemplateTest < Minitest::Test
31
37
  include Liquid
32
38
 
@@ -133,6 +139,17 @@ class TemplateTest < Minitest::Test
133
139
  refute_nil t.resource_limits.assign_score
134
140
  end
135
141
 
142
+ def test_resource_limits_assign_score_nested
143
+ t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
144
+
145
+ t.resource_limits.assign_score_limit = 3
146
+ assert_equal "Liquid error: Memory limits exceeded", t.render
147
+ assert t.resource_limits.reached?
148
+
149
+ t.resource_limits.assign_score_limit = 5
150
+ assert_equal "", t.render!
151
+ end
152
+
136
153
  def test_resource_limits_aborts_rendering_after_first_error
137
154
  t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
138
155
  t.resource_limits.render_score_limit = 50
@@ -225,4 +242,68 @@ class TemplateTest < Minitest::Test
225
242
 
226
243
  assert_equal 'BOB filtered', rendered_template
227
244
  end
245
+
246
+ def test_undefined_variables
247
+ t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
248
+ result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
249
+
250
+ assert_equal '33 32 ', result
251
+ assert_equal 3, t.errors.count
252
+ assert_instance_of Liquid::UndefinedVariable, t.errors[0]
253
+ assert_equal 'Liquid error: undefined variable y', t.errors[0].message
254
+ assert_instance_of Liquid::UndefinedVariable, t.errors[1]
255
+ assert_equal 'Liquid error: undefined variable b', t.errors[1].message
256
+ assert_instance_of Liquid::UndefinedVariable, t.errors[2]
257
+ assert_equal 'Liquid error: undefined variable d', t.errors[2].message
258
+ end
259
+
260
+ def test_undefined_variables_raise
261
+ t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
262
+
263
+ assert_raises UndefinedVariable do
264
+ t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, { strict_variables: true })
265
+ end
266
+ end
267
+
268
+ def test_undefined_drop_methods
269
+ d = DropWithUndefinedMethod.new
270
+ t = Template.new.parse('{{ foo }} {{ woot }}')
271
+ result = t.render(d, { strict_variables: true })
272
+
273
+ assert_equal 'foo ', result
274
+ assert_equal 1, t.errors.count
275
+ assert_instance_of Liquid::UndefinedDropMethod, t.errors[0]
276
+ end
277
+
278
+ def test_undefined_drop_methods_raise
279
+ d = DropWithUndefinedMethod.new
280
+ t = Template.new.parse('{{ foo }} {{ woot }}')
281
+
282
+ assert_raises UndefinedDropMethod do
283
+ t.render!(d, { strict_variables: true })
284
+ end
285
+ end
286
+
287
+ def test_undefined_filters
288
+ t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}")
289
+ filters = Module.new do
290
+ def somefilter3(v)
291
+ "-#{v}-"
292
+ end
293
+ end
294
+ result = t.render({ 'a' => 123, 'x' => 'foo' }, { filters: [filters], strict_filters: true })
295
+
296
+ assert_equal '123 ', result
297
+ assert_equal 1, t.errors.count
298
+ assert_instance_of Liquid::UndefinedFilter, t.errors[0]
299
+ assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message
300
+ end
301
+
302
+ def test_undefined_filters_raise
303
+ t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
304
+
305
+ assert_raises UndefinedFilter do
306
+ t.render!({ 'x' => 'foo' }, { strict_filters: true })
307
+ end
308
+ end
228
309
  end
@@ -3,50 +3,54 @@ require 'test_helper'
3
3
  class ConditionUnitTest < Minitest::Test
4
4
  include Liquid
5
5
 
6
+ def setup
7
+ @context = Liquid::Context.new
8
+ end
9
+
6
10
  def test_basic_condition
7
11
  assert_equal false, Condition.new(1, '==', 2).evaluate
8
12
  assert_equal true, Condition.new(1, '==', 1).evaluate
9
13
  end
10
14
 
11
15
  def test_default_operators_evalute_true
12
- assert_evalutes_true 1, '==', 1
13
- assert_evalutes_true 1, '!=', 2
14
- assert_evalutes_true 1, '<>', 2
15
- assert_evalutes_true 1, '<', 2
16
- assert_evalutes_true 2, '>', 1
17
- assert_evalutes_true 1, '>=', 1
18
- assert_evalutes_true 2, '>=', 1
19
- assert_evalutes_true 1, '<=', 2
20
- assert_evalutes_true 1, '<=', 1
16
+ assert_evaluates_true 1, '==', 1
17
+ assert_evaluates_true 1, '!=', 2
18
+ assert_evaluates_true 1, '<>', 2
19
+ assert_evaluates_true 1, '<', 2
20
+ assert_evaluates_true 2, '>', 1
21
+ assert_evaluates_true 1, '>=', 1
22
+ assert_evaluates_true 2, '>=', 1
23
+ assert_evaluates_true 1, '<=', 2
24
+ assert_evaluates_true 1, '<=', 1
21
25
  # negative numbers
22
- assert_evalutes_true 1, '>', -1
23
- assert_evalutes_true -1, '<', 1
24
- assert_evalutes_true 1.0, '>', -1.0
25
- assert_evalutes_true -1.0, '<', 1.0
26
+ assert_evaluates_true 1, '>', -1
27
+ assert_evaluates_true (-1), '<', 1
28
+ assert_evaluates_true 1.0, '>', -1.0
29
+ assert_evaluates_true (-1.0), '<', 1.0
26
30
  end
27
31
 
28
32
  def test_default_operators_evalute_false
29
- assert_evalutes_false 1, '==', 2
30
- assert_evalutes_false 1, '!=', 1
31
- assert_evalutes_false 1, '<>', 1
32
- assert_evalutes_false 1, '<', 0
33
- assert_evalutes_false 2, '>', 4
34
- assert_evalutes_false 1, '>=', 3
35
- assert_evalutes_false 2, '>=', 4
36
- assert_evalutes_false 1, '<=', 0
37
- assert_evalutes_false 1, '<=', 0
33
+ assert_evaluates_false 1, '==', 2
34
+ assert_evaluates_false 1, '!=', 1
35
+ assert_evaluates_false 1, '<>', 1
36
+ assert_evaluates_false 1, '<', 0
37
+ assert_evaluates_false 2, '>', 4
38
+ assert_evaluates_false 1, '>=', 3
39
+ assert_evaluates_false 2, '>=', 4
40
+ assert_evaluates_false 1, '<=', 0
41
+ assert_evaluates_false 1, '<=', 0
38
42
  end
39
43
 
40
44
  def test_contains_works_on_strings
41
- assert_evalutes_true 'bob', 'contains', 'o'
42
- assert_evalutes_true 'bob', 'contains', 'b'
43
- assert_evalutes_true 'bob', 'contains', 'bo'
44
- assert_evalutes_true 'bob', 'contains', 'ob'
45
- assert_evalutes_true 'bob', 'contains', 'bob'
45
+ assert_evaluates_true 'bob', 'contains', 'o'
46
+ assert_evaluates_true 'bob', 'contains', 'b'
47
+ assert_evaluates_true 'bob', 'contains', 'bo'
48
+ assert_evaluates_true 'bob', 'contains', 'ob'
49
+ assert_evaluates_true 'bob', 'contains', 'bob'
46
50
 
47
- assert_evalutes_false 'bob', 'contains', 'bob2'
48
- assert_evalutes_false 'bob', 'contains', 'a'
49
- assert_evalutes_false 'bob', 'contains', '---'
51
+ assert_evaluates_false 'bob', 'contains', 'bob2'
52
+ assert_evaluates_false 'bob', 'contains', 'a'
53
+ assert_evaluates_false 'bob', 'contains', '---'
50
54
  end
51
55
 
52
56
  def test_invalid_comparation_operator
@@ -65,29 +69,29 @@ class ConditionUnitTest < Minitest::Test
65
69
  @context['array'] = [1, 2, 3, 4, 5]
66
70
  array_expr = VariableLookup.new("array")
67
71
 
68
- assert_evalutes_false array_expr, 'contains', 0
69
- assert_evalutes_true array_expr, 'contains', 1
70
- assert_evalutes_true array_expr, 'contains', 2
71
- assert_evalutes_true array_expr, 'contains', 3
72
- assert_evalutes_true array_expr, 'contains', 4
73
- assert_evalutes_true array_expr, 'contains', 5
74
- assert_evalutes_false array_expr, 'contains', 6
75
- assert_evalutes_false array_expr, 'contains', "1"
72
+ assert_evaluates_false array_expr, 'contains', 0
73
+ assert_evaluates_true array_expr, 'contains', 1
74
+ assert_evaluates_true array_expr, 'contains', 2
75
+ assert_evaluates_true array_expr, 'contains', 3
76
+ assert_evaluates_true array_expr, 'contains', 4
77
+ assert_evaluates_true array_expr, 'contains', 5
78
+ assert_evaluates_false array_expr, 'contains', 6
79
+ assert_evaluates_false array_expr, 'contains', "1"
76
80
  end
77
81
 
78
82
  def test_contains_returns_false_for_nil_operands
79
83
  @context = Liquid::Context.new
80
- assert_evalutes_false VariableLookup.new('not_assigned'), 'contains', '0'
81
- assert_evalutes_false 0, 'contains', VariableLookup.new('not_assigned')
84
+ assert_evaluates_false VariableLookup.new('not_assigned'), 'contains', '0'
85
+ assert_evaluates_false 0, 'contains', VariableLookup.new('not_assigned')
82
86
  end
83
87
 
84
88
  def test_contains_return_false_on_wrong_data_type
85
- assert_evalutes_false 1, 'contains', 0
89
+ assert_evaluates_false 1, 'contains', 0
86
90
  end
87
91
 
88
92
  def test_contains_with_string_left_operand_coerces_right_operand_to_string
89
- assert_evalutes_true ' 1 ', 'contains', 1
90
- assert_evalutes_false ' 1 ', 'contains', 2
93
+ assert_evaluates_true ' 1 ', 'contains', 1
94
+ assert_evaluates_false ' 1 ', 'contains', 2
91
95
  end
92
96
 
93
97
  def test_or_condition
@@ -121,8 +125,8 @@ class ConditionUnitTest < Minitest::Test
121
125
  def test_should_allow_custom_proc_operator
122
126
  Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{right}} }
123
127
 
124
- assert_evalutes_true 'bob', 'starts_with', 'b'
125
- assert_evalutes_false 'bob', 'starts_with', 'o'
128
+ assert_evaluates_true 'bob', 'starts_with', 'b'
129
+ assert_evaluates_false 'bob', 'starts_with', 'o'
126
130
  ensure
127
131
  Condition.operators.delete 'starts_with'
128
132
  end
@@ -131,24 +135,24 @@ class ConditionUnitTest < Minitest::Test
131
135
  @context = Liquid::Context.new
132
136
  @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
133
137
 
134
- assert_evalutes_true VariableLookup.new("one"), '==', VariableLookup.new("another")
138
+ assert_evaluates_true VariableLookup.new("one"), '==', VariableLookup.new("another")
135
139
  end
136
140
 
137
141
  private
138
142
 
139
- def assert_evalutes_true(left, op, right)
140
- assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
143
+ def assert_evaluates_true(left, op, right)
144
+ assert Condition.new(left, op, right).evaluate(@context),
141
145
  "Evaluated false: #{left} #{op} #{right}"
142
146
  end
143
147
 
144
- def assert_evalutes_false(left, op, right)
145
- assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
148
+ def assert_evaluates_false(left, op, right)
149
+ assert !Condition.new(left, op, right).evaluate(@context),
146
150
  "Evaluated true: #{left} #{op} #{right}"
147
151
  end
148
152
 
149
153
  def assert_evaluates_argument_error(left, op, right)
150
154
  assert_raises(Liquid::ArgumentError) do
151
- Condition.new(left, op, right).evaluate(@context || Liquid::Context.new)
155
+ Condition.new(left, op, right).evaluate(@context)
152
156
  end
153
157
  end
154
158
  end # ConditionTest
@@ -77,4 +77,60 @@ class StrainerUnitTest < Minitest::Test
77
77
  assert_kind_of b, strainer
78
78
  assert_kind_of Liquid::StandardFilters, strainer
79
79
  end
80
+
81
+ def test_add_filter_when_wrong_filter_class
82
+ c = Context.new
83
+ s = c.strainer
84
+ wrong_filter = ->(v) { v.reverse }
85
+
86
+ assert_raises ArgumentError do
87
+ s.class.add_filter(wrong_filter)
88
+ end
89
+ end
90
+
91
+ module PrivateMethodOverrideFilter
92
+ private
93
+
94
+ def public_filter
95
+ "overriden as private"
96
+ end
97
+ end
98
+
99
+ def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
100
+ strainer = Context.new.strainer
101
+
102
+ error = assert_raises(Liquid::MethodOverrideError) do
103
+ strainer.class.add_filter(PrivateMethodOverrideFilter)
104
+ end
105
+ assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
106
+ end
107
+
108
+ module ProtectedMethodOverrideFilter
109
+ protected
110
+
111
+ def public_filter
112
+ "overriden as protected"
113
+ end
114
+ end
115
+
116
+ def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
117
+ strainer = Context.new.strainer
118
+
119
+ error = assert_raises(Liquid::MethodOverrideError) do
120
+ strainer.class.add_filter(ProtectedMethodOverrideFilter)
121
+ end
122
+ assert_equal 'Liquid error: Filter overrides registered public methods as non public: public_filter', error.message
123
+ end
124
+
125
+ module PublicMethodOverrideFilter
126
+ def public_filter
127
+ "public"
128
+ end
129
+ end
130
+
131
+ def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
132
+ strainer = Context.new.strainer
133
+ strainer.class.add_filter(PublicMethodOverrideFilter)
134
+ assert strainer.class.filter_methods.include?('public_filter')
135
+ end
80
136
  end # StrainerTest
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.rc1
4
+ version: 4.0.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-08 00:00:00.000000000 Z
11
+ date: 2016-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake