-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
New Performance/DoubleStartEndWith cop #2590
Conversation
Nice!! I was thinking of doing this myself, good to see that someone else did it first! |
# str.end_with?(var1, var2) | ||
class DoubleStartEndWith < Cop | ||
MSG = 'Use `str.%{method}(x, ..., y, ...)` ' \ | ||
'instead of `str.%{method}(x, ...) || str.%{method}(y, ...)`.' |
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.
Rather than showing x, ...
and y, ...
, it would be nicer if the message showed the actual arguments.
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.
Good point. I'll fix it.
One other way this could be improved -- think of things like this: # this will parse as (or (or something str.start_with?('a')) str.start_with?('b'))...
# in other words the 2 nodes you are interested in will not be nested under the same 'or' node
something || str.start_with?('a') || str.start_with?('b')
# another example:
str.start_with?('a') || foo || bar || str.start_with?('b') Evaluation is from left to right, so you have to make sure that there is no node with unknown side effects between the 2 matching calls. |
Another thing which can potentially be done is we can loosen the requirement of the first argument to the first |
It's up to you. I am just suggesting ways to make the PR better. If you don't want to go that far in this PR, and the maintainers want to accept it as is, that is fine. It is already an improvement over what we have. |
Here is a little summary:
If you guys are ok with this, please let me know. I will squash the commits and add an entry to the change log then. |
9ab0eaa
to
1bb60bb
Compare
UPD: This PR is ready to merge now, if everyone is ok with the changes. |
👍 The changes look good. |
'instead of `%{original_code}`.' | ||
|
||
def on_or(node) | ||
receiver, method, args1, args2 = two_start_end_with_calls(node) |
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.
args1 and args2 are not very descriptive variable names.
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.
Fixed
1bb60bb
to
2f42e87
Compare
context 'two #start_with? calls' do | ||
context 'with the same receiver' do | ||
context 'all parameters of the second call are pure' do | ||
let(:source) { 'x.start_with?(a, b) || x.start_with?("c", D)' } |
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 someone who does a bit of FP on the side, I think the usage of the term "pure" here is confusing. Basically you don' t want a function call as an argument, 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.
No -- "pure" means "pure", as in "has no side effects when evaluated". See the definition in Node
.
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.
Yes that's 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.
Well, strictly speaking we can't allow local variables and constants too. There might be some pathalogical cases when .to_str
have side effects:
class Evil
def initialize
@state = 0
end
def to_str
@state = (@state + 1) % 4
@state.to_s
end
end
EVIL = Evil.new
p "1".start_with?(EVIL, EVIL) || "1".start_with?(EVIL)
# => true
p "1".start_with?(EVIL, EVIL, EVIL)
# => false
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.
pure
doesn't mean "has not side effect when evaluated". It means that the same input always maps to the same output, that the only input is parameters (no global, instance vars, etc) and that they are no side effects. I'll check the definition.
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.
Sounds good to me. @alexdowad what do you think?
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. Seems equally confusing with pure?
, to be honest.
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.
All the nodes you're checking seem to me like value nodes (and the complex nodes also reduce down to values). In general there are no method invocation checks and I really think that many people associate the term pure with functions and methods. I'm open to other naming suggestions, but I certainly think we can do better than the current name. I was recently reminded that I didn't like this name when I originally saw it, but the commit was a small part of a huge PR and I was too tired to go into relatively small details. Anyways, guess this is not related to the current PR.
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 #no_side_effects?
too wordy?
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.
My main issue is that such names imply we're dealing with something executable. I'd expect this to apply to send nodes, not to nodes in general. Naming is so hard...
Rebase this and I'll have it merged. |
This cop checks for double `#start_with?` or `#end_with?` calls separated by `||`. In some cases such calls can be replaced with an single `#start_with?`/`#end_with?` call. ```ruby # bad str.start_with?("a") || str.start_with?(Some::CONST) str.start_with?("a", "b") || str.start_with?("c") var1 = ... var2 = ... str.end_with?(var1) || str.end_with?(var2) # good str.start_with?("a", Some::CONST) || str.start_with?(Some::CONST) str.start_with?("a", "b", "c") var1 = ... var2 = ... str.end_with?(var1, var2) ``` The corner cases are when arguments of the second `start_with?` call have side effects, or when the receiver itself has side effects, or when the receiver is not a string at all, or when someone monkey-patched `String#start_with?`
2f42e87
to
494a434
Compare
Rebased it. CI green, no conflicts (yet). |
New Performance/DoubleStartEndWith cop
This is a continuation of #2535 .
This cop checks for double
#start_with?
or#end_with?
callsseparated by
||
. In some cases such calls can be replacedwith an single
#start_with?
/#end_with?
call.The corner cases are when arguments to start_with? have side effects,
or when the receiver itself has side effects, or when the receiver
is not a string at all, or when someone monkey-patched
String#start_with?
Benchmark results are not too convincing because of a big variance, but they show that the single-call approach is at least not slower that the two-call approach.
Wild life occurrences: ManageIQ/manageiq#5558, rails/rails#22374, the aforementioned #2535.