-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Apply error-checking to sort, sort_natural, where, uniq, map, compact filter(s) #1059
Conversation
lib/liquid/standardfilters.rb
Outdated
begin | ||
r = e[property] | ||
r.is_a?(Proc) ? r.call : r | ||
rescue TypeError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I wonder if there's another way to catch this that doesn't involve a rescue
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I discussed this with Sam, and e[property]
must be called in order to verify its own existence. Otherwise, checking if e
has attribute property
beforehand requires knowledge of property
type (e.g.; if e
is an Array
and property
is a string
, then by calling e.[](property)
we pass in a string argument, which causes an error).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a ton of places in here where user input is indexed by other user input. For instance, compact
, uniq
, etc.
I don't really want to add a wrapper method for this but it may be better than duplicating
liquid/lib/liquid/standardfilters.rb
Lines 472 to 475 in b9a2072
rescue TypeError | |
# Cannot index with the given property type (eg. indexing integers with strings | |
# which are only allowed to be indexed by other integers). | |
raise ArgumentError.new("cannot select the property `#{property}`") |
@pushrax good point. Let's add a wrapper method. Something like this: get_property(item, property)
def get_property(item, property)
item[property]
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end Thoughts? |
That seems to make sense. Although I noticed that the methods with array inputs often check if the first (or all) element(s) edit: @Thibaut forgot to tag you. |
Update: I think raising an ArgumentError might cause a backwards compatibility issue because any template that incorrectly uses |
Any input that can trigger a ruby (non-liquid) exception should be changed to a liquid exception, anything that would previously return nil should continue to return nil unless there's a great reason to change it. |
2a5c896
to
aec2ca1
Compare
some notes I just wanted to make:
@pushrax when you get a chance please review my changes :) |
c91fc68
to
73681c9
Compare
lib/liquid/standardfilters.rb
Outdated
if property == "to_liquid".freeze | ||
ary | ||
elsif ary.first.respond_to?(:[]) | ||
ary.map { |e| (e = get_property(e, property)).is_a?(Proc) ? e.call : e } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we implement this without a double map
? This change is making the method slower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Thibaut I was trying to mirror the format of the other methods. Is the time complexity that much different because it still runs linearly right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bigger problem here is that this allocates an extra array now. This should be done without the extra array.
lib/liquid/standardfilters.rb
Outdated
@@ -160,9 +160,9 @@ def where(input, property, target_value = nil) | |||
if ary.empty? | |||
[] | |||
elsif ary.first.respond_to?(:[]) && target_value.nil? | |||
ary.where_present(property) | |||
ary.select { |item| get_property(item, property) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we move this logic into where
/ where_present
instead of here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As-is, this PR deletes those methods, which I think is fine now that get_property
contains the logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be a better way to handle the where
logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental approval.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
a0b70ff
to
199e695
Compare
lib/liquid/standardfilters.rb
Outdated
@@ -160,9 +160,9 @@ def where(input, property, target_value = nil) | |||
if ary.empty? | |||
[] | |||
elsif ary.first.respond_to?(:[]) && target_value.nil? | |||
ary.where_present(property) | |||
ary.select { |item| get_property(item, property) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As-is, this PR deletes those methods, which I think is fine now that get_property
contains the logic.
lib/liquid/standardfilters.rb
Outdated
@@ -394,6 +394,12 @@ def default(input, default_value = ''.freeze) | |||
|
|||
private | |||
|
|||
def get_property(input, property) | |||
input[property] | |||
rescue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason you changed this from explicitly rescuing TypeError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I just forgot. Thanks for catching that 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect performance will be decently higher for most arrays if the extra method call is not done on each iteration. The approach done for where
and where_present
before this PR avoided this. How messy would it be to factor the code in this way for all loops that access a property?
@pushrax with the exception of # Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
applesauce(property) { ary.reject{ |a| a[property].nil? } }
end
end
def applesauce(property)
yield
rescue TypeError
raise Liquid::ArgumentError.new("Cannot select the property '#{property}'")
end how does this sound? |
@pushrax when you get time please tell me what you think |
With a benchmark 👍 to that idea. |
6ac0403
to
6f01eae
Compare
@pushrax I've made the change. Please review when you're available. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost LGTM.
require 'benchmark_driver'
Benchmark.driver do |x|
x.prelude <<~RUBY
array = ["a"] * 10
property = "foo"
def safe_property_check(property)
yield
rescue TypeError
raise
end
RUBY
x.report 'no safe_property_check', <<~RUBY
array.map { |x| x }
RUBY
x.report 'outer safe_property_check', <<~RUBY
safe_property_check(property) do
array.map { |x| x }
end
RUBY
x.report 'inner safe_property_check', <<~RUBY
array.map do |x|
safe_property_check(property) do
x
end
end
RUBY
end |
I played a bit with benchmark and inlining the rescue behaviour preserves current runtime:
of course that literally comes back to the original issue of duplicating logic everywhere. However since it is slightly slower using a method, would it just make sense to inline the |
Adding ~50ns overhead is probably okay for most real cases here, but it's not really much more code to do something like def sort(input, property)
...
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end Usually I wouldn't care so much, but I wouldn't be surprised if some Liquid templates use these filters in tight loops. |
6f01eae
to
c991f1c
Compare
@pushrax I rewrote it with just inlining the rescue blocks just to see what it looks like. It's messy but it does the job better since it performs faster.. does this look better or did I misunderstand your previous comment? |
You could abstract the |
5e70811
to
eeea538
Compare
This is a lot better than before 👍. |
11b53a5
to
ca1f972
Compare
ca1f972
to
c651589
Compare
c651589
to
cec27ea
Compare
eZ Pz. |
Some types (for example
Array
s andInteger
s) cannot be indexed using strings. Previously this would raise a TypeError when attempted. Now we catch that error and returnnil
instead. Original issue was with map but the same issue was presented in many of the other filters. I also inlined the where implementation because therescue
block was no longer necessary in the class method.