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

Add new RSpec/MissingExpectationTargetMethod cop #1893

Merged
merged 1 commit into from
Jun 7, 2024

Conversation

krororo
Copy link
Contributor

@krororo krororo commented Jun 1, 2024

I have noticed that we are using the wrong expect in our application.

expect(something).kind_of? FooClass
is_expected == 42

These will pass the test without error or warning, but the expected behavior would be code like this.

expect(something).to be_a FooClass
is_expected.to eq 42

This PR checks the above code.


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

If you have created a new cop:

  • Added the new cop to config/default.yml.
  • The cop is configured as Enabled: pending in config/default.yml.
  • The cop is configured as Enabled: true in .rubocop.yml.
  • The cop documents examples of good and bad code.
  • The tests assert both that bad code is reported and that good code is not reported.
  • Set VersionAdded: "<<next>>" in default/config.yml.

@krororo krororo requested a review from a team as a code owner June 1, 2024 07:55
def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
return unless expect_block?(node)

check_expect(node)
Copy link
Member

Choose a reason for hiding this comment

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

Can you please add some coverage for this? Do we even need on_block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

on_block is defined to support the expect with block. The test is below.
https://github.com/rubocop/rubocop-rspec/pull/1893/files#diff-5e17efe314d6a76ce15c21be790197c8d4958d545fbb923dc78881b4172f2a3fR36

This was implemented with reference to void_expect.rb.

If I check the node type of the parent, it looks like only on_send would be fine, is that better?
The following is an image of the implementation using only on_send.

        def on_send(node)
          return unless expect?(node)

          if node.parent&.block_type?
            check_expect(node.parent)
          else
            check_expect(node)
          end
        end

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation, it makes total sense to check the block syntax, too.
But it seems that with no regards to the presence or absence of the block it can be detected in on_send, can’t it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed on_block because I could detect it only with on_send.

PATTERN

# @!method expectation_target_method?(node)
def_node_matcher :expectation_target_method?, <<~PATTERN
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def_node_matcher :expectation_target_method?, <<~PATTERN
def_node_matcher :expectation_without_runner?, <<~PATTERN
(send #expect? !#Runners.all …)

Should be able to detect expects what its name tells.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the suggestion. That looks better, so I will fix it.

@pirj
Copy link
Member

pirj commented Jun 1, 2024

Nice! This covers a lot of real life cases.
Can you please run it against real-world-rspec to see what it can detect?

@krororo
Copy link
Contributor Author

krororo commented Jun 2, 2024

Can you please run it against real-world-rspec to see what it can detect?

When I ran it against real-world-rspec, I noticed that it was using send and public_send, so I allowed it.

/path/to/real-world-rspec/activeadmin/spec/helpers/filter_form_helper_spec.rb:482:24: C: RSpec/ExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(body).send if_true, have_field("q[body_cont]")
                       ^^^^
/path/to/real-world-rspec/activeadmin/spec/helpers/filter_form_helper_spec.rb:487:24: C: RSpec/ExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(body).send if_false, have_field("q[body_cont]")
                       ^^^^
/path/to/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:12:30: C: RSpec/ExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Notification.count).public_send(matcher, eq(notification_count))
                             ^^^^^^^^^^^
/path/to/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:20:33: C: RSpec/ExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(notification.topic_id).public_send(matcher, eq(topic_id))
                                ^^^^^^^^^^^
/path/to/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:28:30: C: RSpec/ExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Notification.count).public_send(matcher, eq(notification_count))
                             ^^^^^^^^^^^

@ydah
Copy link
Member

ydah commented Jun 2, 2024

Could you squashed all commits into one?

@krororo krororo force-pushed the add-expectation_target_method branch from eee9e69 to fd88583 Compare June 2, 2024 11:40
@krororo
Copy link
Contributor Author

krororo commented Jun 2, 2024

OK. I squashed commits and force push.

@pirj
Copy link
Member

pirj commented Jun 2, 2024

I would rather not tolerate send/public_send, because:

  • just a few cases out there
  • it’s a dubious DRY practice
  • I’d rather not promote it in any way

Copy link
Member

@ydah ydah left a comment

Choose a reason for hiding this comment

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

About cop name:

* The name explains the offense the cop detects, e.g. `ExtraSpacing`

A name that describes the offense detected by the this cop is recommended, and since it is not considered an offense if there is an ExpectationTarget method, a name that indicates that there is no ExpectationTarget method would be good.

module RuboCop
module Cop
module RSpec
# Checks ExpectationTarget method.
Copy link
Member

@ydah ydah Jun 3, 2024

Choose a reason for hiding this comment

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

imo) The word “Checks” seems to be unclear as to whether it is or is not an offense as a result of the check, although this is a formality that exists in many other cops. I think it would be better if it is explained when it is an offense, or what kind of format is recommended by the cop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have come up with the following explanation, what do you think of this explanation?

Checks if the ExpectationTarget methods `.to`, `not_to` or `to_not` are used.

Copy link
Member

Choose a reason for hiding this comment

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

How about we change the following?

Checks if the RSpec::Expectations::ExpectationTarget methods `.to`, `not_to` or `to_not` are used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I tried the fix but had problems 😢

First, I tried to write it in one line, but was registered an offense by Layout/LineLength.

lib/rubocop/cop/rspec/missing_expectation_target_method.rb:6:81: C: Layout/LineLength: Line is too long. [106/80]
      # Checks if the RSpec::Expectations::ExpectationTarget methods `.to`, `not_to` or `to_not` are used.
                                                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^

Next, the test failed when I included newlines.

  1) config/default.yml does not have newlines in cop descriptions
     Failure/Error: expect(value).not_to include("\n")
       expected "Checks if the RSpec::Expectations::ExpectationTarget methods `.to`,\n`not_to` or `to_not` are used." not to include "\n"
     # ./spec/project/default_config_spec.rb:80:in `block (3 levels) in <top (required)>'
     # ./spec/project/default_config_spec.rb:79:in `each'
     # ./spec/project/default_config_spec.rb:79:in `block (2 levels) in <top (required)>'

In the build_config task, the first line of the document is set to description in default.yml, but it seems that newlines are not allowed.

I tried to simplify the first line and write the details separately as follows.

Checks if `.to`, `not_to` or `to_not` are used.

The RSpec::Expectations::ExpectationTarget must use `to`, `not_to` or
`to_not` to run. Therefore, this cop checks if other methods are used.

@krororo krororo force-pushed the add-expectation_target_method branch from fd88583 to 8c814be Compare June 3, 2024 14:51
@krororo
Copy link
Contributor Author

krororo commented Jun 3, 2024

A name that describes the offense detected by the this cop is recommended, and since it is not considered an offense if there is an ExpectationTarget method, a name that indicates that there is no ExpectationTarget method would be good.

I have considered the name MissingExpectationTargetMethod. What do you think of this name?

@krororo krororo force-pushed the add-expectation_target_method branch from 8c814be to 9677b76 Compare June 4, 2024 14:20
@krororo krororo changed the title Add new RSpec/ExpectationTargetMethod cop Add new RSpec/MissingExpectationTargetMethod cop Jun 4, 2024
PATTERN

def on_send(node)
if node.parent&.block_type?
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if node.parent&.block_type?
add_offense(node.loc.selector) if expectation_without_runner?(node)
end

And remove everything below.
Would it work like that?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I was looking at reducing it to

def on_send(node)
  node = node.parent if node.parent&.block_type?

  expectation_without_runner?(node.parent) do
    add_offense(node.parent.loc.selector)
  end
end

Copy link
Member

Choose a reason for hiding this comment

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

Is that because we want to highlight the offence differently for the block vs value expectations?

Copy link
Collaborator

@bquorning bquorning Jun 5, 2024

Choose a reason for hiding this comment

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

I don’t know. My suggested code works like the code in the PR (the specs pass without change).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry @pirj, I only just now understood what you meant.

Your simpler solution is indeed possible, but I think we’d have to remove RESTRICT_ON_SEND.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

value expectations:

$ ruby-parse -e 'expect(foo).to' 
(send
  (send nil :expect
    (send nil :foo)) :to)

with block:

$ ruby-parse -e 'expect{foo}.to' 
(send
  (block
    (send nil :expect)
    (args)
    (send nil :foo)) :to)

In both cases I wanted to highlight the to location. Is there a better way?
The specs did not pass with expectation_without_runner?(node).

Copy link
Member

Choose a reason for hiding this comment

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

@bquorning Now when you mentioned RESTRICT_ON_SEND, i started understanding why it is implemented the way it is.
No objections either way.
I thought that a concise nodepattern-only implementation is superior, but with restrict on send the choice is not that trivial.
I’m wondering if we allow our users to configure expectations in the dsl, and if we should account for that. That’s my only argument left for the nodepattern solution.

@bquorning bquorning mentioned this pull request Jun 5, 2024
@krororo krororo force-pushed the add-expectation_target_method branch from 9677b76 to 87e0e4a Compare June 5, 2024 15:13
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.

❤️ Thank you @krororo

@bquorning
Copy link
Collaborator

@pirj, @ydah Is this PR ready to merge?

Copy link
Member

@ydah ydah left a comment

Choose a reason for hiding this comment

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

It seems false positive:

/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:6:28: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(7).target).to eq(7)
                           ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:11:33: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(&block).target).to be(block)
                                ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:33:30: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(nil).target).to be_nil
                             ^^^^^^

@ydah
Copy link
Member

ydah commented Jun 7, 2024

another case:

/ydah/real-world-rspec/loomio/spec/controllers/mailer_helpers.rb:26:51: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Nokogiri::HTML(body).css(selector).to_s).length.to be 0
                                                  ^^^^^^

@ydah
Copy link
Member

ydah commented Jun 7, 2024

Is this also?

/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:81:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_be_empty } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:84:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['packageProtectionRule']).to be_blank }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:88:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['errors']).to eq ['Package name pattern has already been taken'] }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:97:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:105:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/packageNamePattern can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:119:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                       ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:131:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
                  ^^^

@ydah
Copy link
Member

ydah commented Jun 7, 2024

Here are the all results of running real-world-rspec:

/ydah/real-world-rspec/activeadmin/spec/helpers/filter_form_helper_spec.rb:482:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(body).send if_true, have_field("q[body_cont]")
                       ^^^^
/ydah/real-world-rspec/activeadmin/spec/helpers/filter_form_helper_spec.rb:487:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(body).send if_false, have_field("q[body_cont]")
                       ^^^^
/ydah/real-world-rspec/cartodb/spec/requests/carto/api/apps_controller_spec.rb:241:38: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(response.body[:apps]).present?.should be true
                                     ^^^^^^^^
/ydah/real-world-rspec/cartodb/spec/requests/carto/api/apps_controller_spec.rb:242:37: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(response.body[:url]).present?.should be true
                                    ^^^^^^^^
/ydah/real-world-rspec/cartodb/spec/requests/carto/api/custom_visualizations_controller_spec.rb:265:48: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(response.body[:visualizations]).present?.should be true
                                               ^^^^^^^^
/ydah/real-world-rspec/cartodb/spec/requests/carto/api/custom_visualizations_controller_spec.rb:266:37: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(response.body[:url]).present?.should be true
                                    ^^^^^^^^
/ydah/real-world-rspec/chatwoot/spec/controllers/api/v1/accounts/contacts_controller_spec.rb:103:64: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(response_body['payload'].first['last_seen_at']).present?
                                                               ^^^^^^^^
/ydah/real-world-rspec/chatwoot/spec/models/platform_app_permissible_spec.rb:18:43: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { expect(platform_app_permissible).present? }
                                          ^^^^^^^^
/ydah/real-world-rspec/chatwoot/spec/models/platform_app_spec.rb:22:31: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { expect(platform_app).present? }
                              ^^^^^^^^
/ydah/real-world-rspec/chatwoot/spec/models/user_spec.rb:75:31: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      expect(sso_auth_token1).present?
                              ^^^^^^^^
/ydah/real-world-rspec/chatwoot/spec/models/user_spec.rb:76:31: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      expect(sso_auth_token2).present?
                              ^^^^^^^^
/ydah/real-world-rspec/chef/spec/unit/resource/macos_user_defaults_spec.rb:62:48: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      expect(resource.get_preference resource).eql? test_value
                                               ^^^^
/ydah/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:12:30: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Notification.count).public_send(matcher, eq(notification_count))
                             ^^^^^^^^^^^
/ydah/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:20:33: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(notification.topic_id).public_send(matcher, eq(topic_id))
                                ^^^^^^^^^^^
/ydah/real-world-rspec/discourse/spec/requests/notifications_controller_spec.rb:28:30: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Notification.count).public_send(matcher, eq(notification_count))
                             ^^^^^^^^^^^
/ydah/real-world-rspec/engine/spec/services/locomotive/content_asset_service_spec.rb:14:31: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { expect(subject.size) == 1 }
                              ^^
/ydah/real-world-rspec/gitlabhq/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb:373:61: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(page.find('.discussion-with-resolve-btn')).not.to have_selector('.btn', text: 'Resolve thread')
                                                            ^^^
/ydah/real-world-rspec/gitlabhq/spec/features/projects/settings/project_settings_spec.rb:92:27: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    expect(panel[:class]).send(is_collapsed ? 'not_to' : 'to', include('expanded'))
                          ^^^^
/ydah/real-world-rspec/gitlabhq/spec/finders/design_management/versions_finder_spec.rb:26:38: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(design_or_collection).is_a?(DesignManagement::DesignCollection)
                                     ^^^^^
/ydah/real-world-rspec/gitlabhq/spec/finders/design_management/versions_finder_spec.rb:59:42: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
            expect(design_or_collection).is_a?(DesignManagement::DesignCollection)
                                         ^^^^^
/ydah/real-world-rspec/gitlabhq/spec/graphql/resolvers/container_repository_tags_resolver_spec.rb:117:32: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(resolver(args)).is_a? Gitlab::Graphql::ExternallyPaginatedArray
                               ^^^^^
/ydah/real-world-rspec/gitlabhq/spec/migrations/20230214181633_finalize_ci_build_needs_big_int_conversion_spec.rb:34:33: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
        expect(described_class).send(
                                ^^^^
/ydah/real-world-rspec/gitlabhq/spec/models/merge_request_spec.rb:468:33: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(merge_request).send(to_or_not_to, receive(:validate_reviewer_size_length))
                                ^^^^
/ydah/real-world-rspec/gitlabhq/spec/models/merge_request_spec.rb:512:35: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
            expect(merge_request).send(to_or_not_to, receive(:validate_target_project))
                                  ^^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb:56:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb:67:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb:83:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                       ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:88:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_be_empty } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:91:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['containerRegistryProtectionRule']).to be_blank }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:95:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['errors']).to eq ['Repository path pattern has already been taken'] }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:104:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:112:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/repositoryPathPattern can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:126:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                       ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/container_registry/protection/rule/update_spec.rb:138:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap do
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/delete_spec.rb:39:20: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  it { is_expected.tap { expect_graphql_errors_to_be_empty } }
                   ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:81:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_be_empty } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:84:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['packageProtectionRule']).to be_blank }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:88:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect(mutation_response['errors']).to eq ['Package name pattern has already been taken'] }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:97:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/pushProtectedUpToAccessLevel can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:105:22: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
    it { is_expected.tap { expect_graphql_errors_to_include(/packageNamePattern can't be blank/) } }
                     ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:119:24: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } }
                       ^^^
/ydah/real-world-rspec/gitlabhq/spec/requests/api/graphql/mutations/packages/protection/rule/update_spec.rb:131:19: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
      is_expected.tap { expect_graphql_errors_to_include(/'packages_protected_packages' feature flag is disabled/) }
                  ^^^
/ydah/real-world-rspec/gitlabhq/spec/support/helpers/after_next_helpers.rb:34:79: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect_next_instance_of(klass, *args) { |instance| expect(instance).send(msg, condition) }
                                                                              ^^^^
/ydah/real-world-rspec/gitlabhq/spec/support/helpers/after_next_helpers.rb:36:78: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          allow_next_instance_of(klass, *args) { |instance| expect(instance).send(msg, condition) }
                                                                             ^^^^
/ydah/real-world-rspec/loomio/spec/controllers/mailer_helpers.rb:26:51: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
  expect(Nokogiri::HTML(body).css(selector).to_s).length.to be 0
                                                  ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:6:28: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(7).target).to eq(7)
                           ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:11:33: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(&block).target).to be(block)
                                ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/expectations/expectation_target_spec.rb:33:30: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
          expect(expect(nil).target).to be_nil
                             ^^^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/matchers/built_in/exist_spec.rb:14:29: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
            expect(subject).send(expect_method, exist)
                            ^^^^
/ydah/real-world-rspec/rspec-expectations/spec/rspec/matchers/built_in/exist_spec.rb:115:31: C: RSpec/MissingExpectationTargetMethod: Use .to, .not_to or .to_not to set an expectation.
              expect(subject).send(expect_method, exist)
                              ^^^^

@pirj
Copy link
Member

pirj commented Jun 7, 2024

rspec-expectations are fine, these are tests for some internals.

Loomio is a legitimate offence

I’m not sure I fully understand GitLab’s is_expected.tap { … }, I can guess it’s a form of the implicit block expectation syntax which will be removed from RSpec 4. I have no desire to support or tolerate it in our cops.

ActiveAdmin’s expect.send if_true is an ugly hack.
So is Discourse’s public_send(matcher).
One may argue that the cop is Unsafe by definition, as we by design flag working code as offences.
But the cop name says “missing expectation target method”. Can we consider “concealed” as “missing”? I’m more towards “yes”, and to/not_to/to_not to always be explicitly aand literally specified.

‘ expect(response.body[:url]).present?.should be true’ seems to be a mistake.

expect(page.find('.discussion-with-resolve-btn')).not.to
.not.to? How does this work?

expect(Nokogiri::HTML(body).css(selector).to_s).length.to be 0 - how does it even work? Is there a length on the expectationtarget that returns self?

I’ve looked closely at every offence on this list above, and to me, this falls in three categories:

  1. Legitimate offences
  2. Implicit block syntax - we don’t want to support
  3. Non-literal call of a runner with send/public_send. Don’t want to support

What is your take on this @bquorning @ydah ?

@bquorning
Copy link
Collaborator

After a quick glance at the list I’d say the cop is working fine. For the edge cases (e.g. rspec-expectations) people will probably gladly add a rubocop:disable comment.

For all other cases I think people should be happy that we point out their unclear code. I mean, that’s kinda our job, right?

Copy link
Member

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

Thank you!

Copy link
Member

@ydah ydah left a comment

Choose a reason for hiding this comment

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

I can guess it's a form of the implicit block expectation syntax which will be removed from RSpec 4.

Ah, I see. Thank you.

.not.to is a v2.x notation.

I have no idea about expect(Nokogiri::HTML(body).css(selector).to_s).length.to be 0. I actually tried to run it in the environment at hand, and it gives me an error.

  1) sandbox something test
     Failure/Error: expect('').length.to be 0
     
     NoMethodError: undefined method `length'.
       undefined method `length' for an instance of RSpec::Expectations::ValueExpectationTarget
     # . /spec/sandbox_spec.rb:8:in `block (2 levels) in <top (required)>'

I was wondering if there were any cases where .to or something like that was written but was offensed, would users be confused because the offense message is "Use .to, .not_to or .to_not to set an expectation." I was wondering if there was a problem with that, but there doesn't seem to be.
It looks good! Thanks!

@ydah ydah merged commit ea7a5d8 into rubocop:master Jun 7, 2024
24 checks passed
@krororo
Copy link
Contributor Author

krororo commented Jun 7, 2024

@pirj @bquorning @ydah
Thank you for the many reviews! And sorry for the inconvenience 🙇

@krororo krororo deleted the add-expectation_target_method branch June 7, 2024 12:32
@pirj
Copy link
Member

pirj commented Jun 7, 2024

Great job and an absolutely wonderful cop. Thanks for the contribution, @krororo !

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.

4 participants