-
-
Notifications
You must be signed in to change notification settings - Fork 276
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
Improve cops by using TopLevelGroup #977
Conversation
9b3146f
to
32188de
Compare
lib/rubocop/cop/rspec/file_path.rb
Outdated
node, | ||
message: format(MSG, suffix: glob) | ||
) | ||
ensure_correct_file_path(send_node, described_class, 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.
Code climate insisted on breaking this method apart.
@@ -35,11 +54,32 @@ | |||
expect(cop.config_to_allow_offenses[:exclude_limit]).to eq('Max' => 4) | |||
end | |||
|
|||
it 'ignores non-spec context methods' do |
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.
It wasn't ignoring. Just the example didn't have enough nesting to trigger the offence.
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
82099fd
to
74e094f
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.
@Darhazer Addressed your notes and went a few steps further in hope to foresee your next round of code review notes :)
@bquorning Please take a look.
I didn't do the performance impact. Our spec/rubocop speeds seem to be in the range of a measurement error.
!(send #{RSPEC} :describe ... (hash <#rails_metadata? ...>)) ] | ||
... | ||
) | ||
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.
Collapsed several significantly overlapping node patterns into one.
Moved guard statements into node pattern as well.
There still seems to be some redundancy in this node pattern, similarly to SubjectStub
:
{
(block (send nil? :subject (sym $_)) args ...)
(block (send nil? $:subject) args ...)
}
I'll open a ticket for rubocop-ast
.
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 [ ...]
a new syntax for { ... }
? Does it work on the minimum supported rubocop version or should we bump the requirements?
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 logical "and", it's around for a while, just not too frequently used.
I find it too hard to reason about boolean login, this node pattern sounds like:
a block
with a send of `describe` to `RSpec`/nil?
where the first argument is
neither a const not a string resembling a const
*and* the rest of the arguments don't contain a hash with Rails-like metadata
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'm confused by the (send #rspec? :describe
part in the second argument 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.
@Darhazer Why?
It's basically the same as previously (which I forgot to remove during merge resolution):
def_node_matcher :describe_with_rails_metadata?, <<-PATTERN
(send #rspec? :describe !const ...
(hash <#rails_metadata? ...>)
)
PATTERN
!const
check is not necessary anymore, it's checked in the first part 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.
I too find this node pattern a bit cryptic. Perhaps a bit more duplication can be justified if it makes this code easier to read?
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 guys. Split this node pattern into two parts. Looks more readable now.
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.
FYI the shiny newly released rubocop-ast
allows comments in patterns
second_argument = second_argument(node) | ||
|
||
return unless second_argument | ||
return if second_argument.str_content.start_with?('#', '.') |
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'm ok to turn this into a method and pushing this into the node pattern, but decided not to fix everything in one go.
node, | ||
message: format(MSG, suffix: glob) | ||
) | ||
ensure_correct_file_path(send_node, described_class, 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.
Code climate insisted on this extraction.
def on_block(node) | ||
return unless respond_to?(:on_top_level_group) | ||
return unless top_level_group?(node) | ||
def on_new_investigation |
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 is a hair more efficient than on_block
.
def top_level_nodes(node) | ||
if node.begin_type? | ||
node.children | ||
elsif node.module_type? || node.class_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.
Previously,
module A
class B
RSpec.describe 'C' do
was missed.
context 'when bar' do | ||
context 'when baz' do | ||
context 'when qux' do | ||
^^^^^^^^^^^^^^^^^^ Maximum example group nesting exceeded [4/3]. |
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.
shared_examples_for
is not counted.
If we count it in, even one of our own specs triggers an offence:
RSpec.describe RuboCop::Cop::RSpec::PredicateMatcher, :config do
context 'when enforced style is `explicit`' do
shared_examples 'explicit common' do
context 'when custom matchers are allowed' do
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Maximum example group nesting exceeded [4/3].
Ping @bquorning @Darhazer |
!(send #{RSPEC} :describe ... (hash <#rails_metadata? ...>)) ] | ||
... | ||
) | ||
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.
Is [ ...]
a new syntax for { ... }
? Does it work on the minimum supported rubocop version or should we bump the requirements?
lib/rubocop/rspec/top_level_group.rb
Outdated
def on_new_investigation | ||
top_level_groups.each do |node| | ||
if respond_to?(:on_top_level_example_group) | ||
example_group?(node, &method(:on_top_level_example_group)) |
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.
and we can have the same for other methods like describe
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.
Yep, if we'll eventually need them.
Initially, I was going to add code that would call on_top_level_shared_group
, but that would only count one usage, so decided not to add it just now.
d829ec5
to
b02f725
Compare
Simplified TopLevelGroup by introducing dummy methods. |
b02f725
to
9634e85
Compare
@bquorning @Darhazer ping |
33399eb
to
89c3550
Compare
def single_top_level_group? | ||
top_level_groups.one? | ||
def top_level_groups | ||
@top_level_groups ||= |
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 memoization needs to be reset on each new investigation, right?
In all cases it is recommended to to invalidate your cache data during the
on_new_investigation
callback
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 was following this note:
Right now a new cop instance is created for every file
There might have been some changes after the doc has been introduced.
As a quick experiment, I've set a breakpoint in random cop's initialize
and ran rubocop
(0.88.0):
From: /Users/pirj/source/rubocop-rspec/lib/rubocop/cop/rspec/described_class.rb:63 RuboCop::Cop::RSpec::DescribedClass#initialize:
61: def initialize(*args)
62: require 'pry'; binding.pry
=> 63: super
64: end
[1] pry(#<RuboCop::Cop::RSpec::DescribedClass>)>
.
From: /Users/pirj/source/rubocop-rspec/lib/rubocop/cop/rspec/described_class.rb:63 RuboCop::Cop::RSpec::DescribedClass#initialize:
61: def initialize(*args)
62: require 'pry'; binding.pry
=> 63: super
64: end
[1] pry(#<RuboCop::Cop::RSpec::DescribedClass>)>
.
From: /Users/pirj/source/rubocop-rspec/lib/rubocop/cop/rspec/described_class.rb:63 RuboCop::Cop::RSpec::DescribedClass#initialize:
61: def initialize(*args)
62: require 'pry'; binding.pry
=> 63: super
64: end
[1] pry(#<RuboCop::Cop::RSpec::DescribedClass>)>
.
From: /Users/pirj/source/rubocop-rspec/lib/rubocop/cop/rspec/described_class.rb:63 RuboCop::Cop::RSpec::DescribedClass#initialize:
61: def initialize(*args)
62: require 'pry'; binding.pry
=> 63: super
64: end
[1] pry(#<RuboCop::Cop::RSpec::DescribedClass>)>
.
@marcandre Can you please shed light on this? Isn't this doc misleading?
I used to think it would be quite hard to reuse cop instances since depending on the directory they may receive different config (due to directory-local .rubocop.yml
).
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 cop mentioned in the referenced discussion, SurroundingSpace
does reset its state:
def on_new_investigation
@token_table = nil
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.
What's even more strange:
def on_new_investigation
print "N"
super
end
def initialize(*args)
print "#"
super
end
outputs:
##.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#C#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#N.#.
reduced for better visibility:
#.#.#.#.#.#.#.#.#N.#N.#N.#N.#N.#N.#N.
🤔
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.
@pirj Currently, cops being persisted is opt-in. That being said, I'm wondering if a cop that implements on_new_investigation
shouldn't be persisted by default.
BTW, different config => new cop, but that's a very rare setup.
It looks like the cop is initialized a bunch of times before doing the actual run, that seems like a bug somewhere, I'll check it out.
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.
Cop specs won't be bringing memoization to the light, will they? With all numerous well-unknown RuboCop extensions this can have quite an impact.
Yes, that was my thinking too.
I don't see a reason to disrespect this option.
Absolutely :-)
However, does it make sense to keep this notice for the rest of the cops
You mean the recommendation to clear the data with on_new_investigation
? I guess it's not necessary. I should also be clearer reword the "will be called on different if" to "will not be called unless"...
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 would remove the whole "If your class uses instance variables" section, as it's more confusing than helpful considering the current "one source" - "one cop instance" behaviour.
Even for those cops that need to be persisted, I can think of global variables class variables to persist the data between the runs.
By the way, there's one more thing that can impact those persisted cops - --parallel
. Do we parallelize by cops or by source?
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 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.
Even for those cops that need to be persisted, I can think of class variables to persist the data between the runs.
Not quite... See rubocop/rubocop#7968 if interested
By the way, there's one more thing that can impact those persisted cops - --parallel. Do we parallelize by cops or by source?
I imagine it's by source, but we could segregate those cops that need it; I suspect they simply run them without --parallel
though.
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.
@bquorning As a bottom-line: I wouldn't worry about @top_level_groups ||=
used here unless any of the cops would decide to opt-in for reusage and use TopLevelGroup
which I highly doubt.
!(send #{RSPEC} :describe ... (hash <#rails_metadata? ...>)) ] | ||
... | ||
) | ||
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.
I too find this node pattern a bit cryptic. Perhaps a bit more duplication can be justified if it makes this code easier to read?
7bcab69
to
edddcfe
Compare
edddcfe
to
7714312
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.
I think the changes look very good now. I’m curious to hear @Darhazer comment as well.
Example groups were not nested enough to demonstrate the offence. Describing inside a module and a class is actually quite legitimate, but the limit for nested example groups still applies.
TopLevelDescribe was only able to detect top-level `describe`. TopLevelGroup had a deficiency of missing top-level example groups wrapped in module and class namespaces.
Also, shared groups now
7714312
to
130e720
Compare
Loud thinking describe Weather do
let(:temp) { 20.celsius }
context "when it's warm" do
let(:temp) { 30.celsius } # <== should it count as an additional `let`, or should it collapse with an existing one? |
I assumed it was one. It makes even less sense to me to count the repeated |
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow #925 (comment) - it ignored non-describe top-level example groups #925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - #932 - #977
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
Why? - it was slow rubocop/rubocop-rspec#925 (comment) - it ignored non-describe top-level example groups rubocop/rubocop-rspec#925 (comment) `TopLevelGroup` is a modern replacement for `TopLevelDescribe`. Examples how to migrate cops from TopLevelDescribe to TopLevelGroup: - rubocop/rubocop-rspec#932 - rubocop/rubocop-rspec#977
The previously used
TopLevelDescribe
suffered a few flaws:describe
top-level example groups RSpec/SubjectStub. Refactor and decrease complexity #925 (comment)This pull request fixes those problems by using a newly introduced
TopLevelGroup
.Also fixes a flaw in
TopLevelGroup
when example groups wrapped insidemodule
orclass
were missed.NOT DONE: I'm on the fence if we should deprecate
TopLevelDescribe
before removing it. We most probably don't have much releases left before 2.0, and the majority of people will jump straight to 2.0 skipping that pre-2.0 release. We may add this pull request (and others that replace usages ofTopLevelDescribe
forTopLevelGroup
) as reference examples for migration.Fixes #948
Before submitting the PR make sure the following are checked:
master
(if not - rebase it).CHANGELOG.md
if the new code introduces user-observable changes.bundle exec rake
) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).