-
-
Notifications
You must be signed in to change notification settings - Fork 277
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
Add ImplicitBlockExpectation cop #789
Add ImplicitBlockExpectation cop #789
Conversation
It could be written with the use of def_node_matcher :lambda?, <<-PATTERN
{
(send (const nil? :Proc) :new)
(send nil? :proc)
(send nil? :lambda)
}
PATTERN
def_node_matcher :lambda_block, '$(block #lambda? ...)'
def_node_search :subject_block, Subject::ALL.block_pattern
def on_block(node)
subject_block(node) do |block_node|
lambda_block(block_node.body) { |lambda| add_offense(lambda) }
end
end I'm wary with Which do you prefer, @Darhazer, the current or the one above? |
def_node_search will perform the recursive search on each block. If you go in this direction, you should limit the search only in the top level describe, to avoid wasting CPU cycles. |
d04e9f9
to
571542f
Compare
Thanks, pushed the former solution without |
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.
Let fist merge the style guide though
Agree. 👍 |
Just noticed that the cop flags |
Fair enough. I'll take care of that, will add detection of |
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.
Added notes to facilitate code review.
@bquorning Can you please take a look?
describe do | ||
subject { -> { boom } } | ||
it { should change { something }.to(new_value) } | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid implicit block expectations. |
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 range is not ideal, but I decided to avoid complicating the cop for the sake of highlighting should
itself.
The difference is that is_expected
is a self-contained send
, while for should
it's the whole should(change { something }.to(new_value))
.
|
||
def on_send(node) | ||
implicit_expect(node) do |implicit_expect| | ||
subject = nearest_subject(implicit_expect) |
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.
We're finding the nearest subject, and only then checking if it's a lambda subject.
Check this example.
def nearest_subject(node) | ||
node | ||
.each_ancestor(:block) | ||
.lazy |
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.
lazy
is here to avoid mapping/selecting through all of the ancestors just to find the nearest subject.
end | ||
|
||
def multi_statement_example_group?(node) | ||
example_group_with_body?(node) && node.body.begin_type? |
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.
Non-begin_type
blocks (example groups) are no interest for us, they don't have other blocks than those we came from, and we're searching for subject
. If there are at least two statements in the block, it's a begin_type
.
(block _ _args $(block #lambda? ...)) | ||
PATTERN | ||
|
||
def_node_matcher :implicit_expect, <<-PATTERN |
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.
Shamelessly borrowed from ImplicitSubject
. Not sure if it makes sense to extract this with just two usages.
0df2736
to
a92f085
Compare
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.
Excellent work. Few minor comments
0c8450f
to
7f135df
Compare
7f135df
to
02e41c7
Compare
.lazy | ||
.select { |block_node| multi_statement_example_group?(block_node) } | ||
.map { |block_node| find_subject(block_node) } | ||
.find(&:itself) |
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.
This line is equivalent to .reject(&:nil?).first
, right? Just trying to understand.
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, I think those are equivalent.
Thank you! |
And thank you for the new cop :-) |
Based on https://rspec.rubystyle.guide/#implicit-block-expectations
The implicit syntax is discouraged to use by RSpec Core team and the majority of voters.
There were no good arguments for using the syntax except for brevity and avoiding repetition, but there are better options to achieve the same goal, e.g. by extracting the lengthy block to methods (instead of putting it inside the lambda).
It's barely possible to restrict the usage of this syntax on RSpec side. It's possible to detect if a matcher supports block expectations, but it's impossible to detect if it exclusively supports block expectations and not value expectations. A matcher may support both styles at the same time.
An attempt that results in 86 spec failures pirj/rspec-expectations@a7f0d7a
References:
https://blog.rubystyle.guide/rspec/2019/07/17/rspec-implicit-block-syntax.html
https://www.reddit.com/r/ruby/comments/cejl3q/call_for_discussion_rspec_implicit_block/
https://lobste.rs/s/e8yxmd/call_for_discussion_rspec_implicit_block
rubocop/rspec-style-guide#76
rubocop/rspec-style-guide#103
Before submitting the PR make sure the following are checked:
master
(if not - rebase it).bundle exec rake
) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).If you have created a new cop:
config/default.yml
.Tested on a 10k+ files repo, no false offences raised.