Skip to content
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

Merged
merged 7 commits into from Aug 4, 2020
Merged

Conversation

pirj
Copy link
Member

@pirj pirj commented Jul 22, 2020

The previously used TopLevelDescribe suffered a few flaws:

This pull request fixes those problems by using a newly introduced TopLevelGroup.
Also fixes a flaw in TopLevelGroup when example groups wrapped inside module or class 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 of TopLevelDescribe for TopLevelGroup) as reference examples for migration.

Fixes #948


Before submitting the PR make sure the following are checked:

  • Feature branch is up-to-date with master (if not - rebase it).
  • Squashed related commits together.
  • Added tests.
  • Updated documentation.
  • Added an entry to the CHANGELOG.md if the new code introduces user-observable changes.
  • The build (bundle exec rake) passes (be sure to run this locally, since it may produce updated documentation that you will need to commit).

@pirj pirj self-assigned this Jul 22, 2020
@pirj pirj requested review from Darhazer and bquorning July 22, 2020 22:32
@pirj pirj force-pushed the replace-top_level_describe branch 2 times, most recently from 9b3146f to 32188de Compare July 22, 2020 22:43
node,
message: format(MSG, suffix: glob)
)
ensure_correct_file_path(send_node, described_class, arguments)
Copy link
Member Author

@pirj pirj Jul 22, 2020

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.

spec/rubocop/cop/rspec/multiple_describes_spec.rb Outdated Show resolved Hide resolved
@@ -35,11 +54,32 @@
expect(cop.config_to_allow_offenses[:exclude_limit]).to eq('Max' => 4)
end

it 'ignores non-spec context methods' do
Copy link
Member Author

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.

pirj added a commit that referenced this pull request Jul 23, 2020
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
@pirj pirj mentioned this pull request Jul 23, 2020
7 tasks
@pirj pirj marked this pull request as draft July 23, 2020 15:48
@pirj pirj force-pushed the replace-top_level_describe branch from 82099fd to 74e094f Compare July 23, 2020 15:50
Copy link
Member Author

@pirj pirj left a 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
Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

@pirj pirj Jul 29, 2020

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

Copy link
Member

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 [ ]

Copy link
Member Author

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 [ ].

Copy link
Collaborator

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?

Copy link
Member Author

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.

Copy link
Contributor

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?('#', '.')
Copy link
Member Author

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.

lib/rubocop/cop/rspec/file_path.rb Show resolved Hide resolved
node,
message: format(MSG, suffix: glob)
)
ensure_correct_file_path(send_node, described_class, arguments)
Copy link
Member Author

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
Copy link
Member Author

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.

lib/rubocop/rspec/top_level_group.rb Outdated Show resolved Hide resolved
def top_level_nodes(node)
if node.begin_type?
node.children
elsif node.module_type? || node.class_type?
Copy link
Member Author

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].
Copy link
Member Author

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].

@pirj pirj requested a review from Darhazer July 23, 2020 20:42
@pirj pirj marked this pull request as ready for review July 23, 2020 20:42
@pirj
Copy link
Member Author

pirj commented Jul 28, 2020

Ping @bquorning @Darhazer

!(send #{RSPEC} :describe ... (hash <#rails_metadata? ...>)) ]
...
)
PATTERN
Copy link
Member

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/cop/rspec/describe_method.rb Show resolved Hide resolved
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))
Copy link
Member

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

Copy link
Member Author

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.

lib/rubocop/rspec/top_level_group.rb Outdated Show resolved Hide resolved
@pirj pirj force-pushed the replace-top_level_describe branch 2 times, most recently from d829ec5 to b02f725 Compare July 29, 2020 19:59
@pirj
Copy link
Member Author

pirj commented Jul 29, 2020

Simplified TopLevelGroup by introducing dummy methods.
I've removed TopLevelGroup's checks if those methods exist in the including class, since of TopLevelGroup is included, that means that one of those methods is overridden. Hence, we can use dummy methods and call them instead of checking if they exist in the consumer.

@pirj pirj force-pushed the replace-top_level_describe branch from b02f725 to 9634e85 Compare July 29, 2020 20:06
@pirj
Copy link
Member Author

pirj commented Aug 1, 2020

@bquorning @Darhazer ping

@pirj pirj force-pushed the replace-top_level_describe branch 2 times, most recently from 33399eb to 89c3550 Compare August 2, 2020 16:48
lib/rubocop/rspec/top_level_group.rb Show resolved Hide resolved
lib/rubocop/cop/rspec/file_path.rb Show resolved Hide resolved
lib/rubocop/cop/rspec/file_path.rb Show resolved Hide resolved
def single_top_level_group?
top_level_groups.one?
def top_level_groups
@top_level_groups ||=
Copy link
Collaborator

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

https://github.com/rubocop-hq/rubocop/blob/6b36c4490c2d7c41349e4f71721bf0cf91920578/docs/modules/ROOT/pages/v1_upgrade_notes.adoc#if-your-class-uses-instance-variables

Copy link
Member Author

@pirj pirj Aug 2, 2020

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).

Copy link
Member Author

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

Copy link
Member Author

@pirj pirj Aug 2, 2020

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.

🤔

Copy link
Contributor

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.

Copy link
Contributor

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"...

Copy link
Member Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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.

Copy link
Member Author

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.

lib/rubocop/rspec/top_level_group.rb Show resolved Hide resolved
lib/rubocop/cop/rspec/file_path.rb Show resolved Hide resolved
lib/rubocop/cop/rspec/describe_method.rb Show resolved Hide resolved
!(send #{RSPEC} :describe ... (hash <#rails_metadata? ...>)) ]
...
)
PATTERN
Copy link
Collaborator

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?

@pirj pirj force-pushed the replace-top_level_describe branch 6 times, most recently from 7bcab69 to edddcfe Compare August 2, 2020 23:29
Copy link
Collaborator

@bquorning bquorning left a 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.
@pirj pirj force-pushed the replace-top_level_describe branch from 7714312 to 130e720 Compare August 3, 2020 15:58
@pirj pirj requested a review from Darhazer August 3, 2020 19:27
@pirj
Copy link
Member Author

pirj commented Aug 3, 2020

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?

@marcandre
Copy link
Contributor

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 let.

@pirj pirj merged commit 90a6067 into master Aug 4, 2020
@pirj pirj deleted the replace-top_level_describe branch August 4, 2020 09:29
pirj added a commit that referenced this pull request Oct 22, 2020
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
pirj added a commit that referenced this pull request Oct 22, 2020
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
pirj added a commit that referenced this pull request Oct 22, 2020
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
pirj added a commit that referenced this pull request Nov 2, 2020
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
@pirj pirj mentioned this pull request Nov 5, 2020
3 tasks
pirj added a commit to rubocop/rubocop-capybara that referenced this pull request Dec 29, 2022
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
ydah pushed a commit to rubocop/rubocop-factory_bot that referenced this pull request Apr 13, 2023
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
ydah pushed a commit to rubocop/rubocop-rspec_rails that referenced this pull request Mar 27, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use TopLevelGroup instead of TopLevelDescribe
4 participants