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

[Fix #10145] Update Style/SelectByRegexp to ignore cases where the receiver appears to be a hash #10155

Merged
merged 1 commit into from Oct 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/fix_update_styleselectbyregexp_to_ignore.md
@@ -0,0 +1 @@
* [#10145](https://github.com/rubocop/rubocop/issues/10145): Update `Style/SelectByRegexp` to ignore cases where the receiver appears to be a hash. ([@dvandersluis][])
33 changes: 30 additions & 3 deletions lib/rubocop/cop/style/select_by_regexp.rb
Expand Up @@ -3,9 +3,15 @@
module RuboCop
module Cop
module Style
# This cop looks for places where an subset of an array
# is calculated based on a `Regexp` match, and suggests `grep` or
# `grep_v` instead.
# This cop looks for places where an subset of an Enumerable (array,
# range, set, etc.; see note below) is calculated based on a `Regexp`
# match, and suggests `grep` or `grep_v` instead.
#
# NOTE: Hashes do not behave as you may expect with `grep`, which
# means that `hash.grep` is not equivalent to `hash.select`. Although
# RuboCop is limited by static analysis, this cop attempts to avoid
# registering an offense when the receiver is a hash (hash literal,
# `Hash.new`, `Hash#[]`, or `to_h`/`to_hash`).
#
# NOTE: `grep` and `grep_v` were optimized when used without a block
# in Ruby 3.0, but may be slower in previous versions.
Expand All @@ -16,6 +22,10 @@ module Style
# not be created by `grep`, but may have previously been relied
# upon after the `match?` or `=~` call.
#
# Additionally, the cop cannot guarantee that the receiver of
# `select` or `reject` is actually an array by static analysis,
# so the correction may not be actually equivalent.
#
# @example
# # bad (select or find_all)
# array.select { |x| x.match? /regexp/ }
Expand Down Expand Up @@ -49,6 +59,16 @@ class SelectByRegexp < Base
}
PATTERN

# Returns true if a node appears to return a hash
# @!method creates_hash?(node)
def_node_matcher :creates_hash?, <<~PATTERN
{
(send (const _ :Hash) {:new :[]} ...)
(block (send (const _ :Hash) :new ...) ...)
(send _ { :to_h :to_hash } ...)
}
PATTERN

# @!method calls_lvar?(node, name)
def_node_matcher :calls_lvar?, <<~PATTERN
{
Expand All @@ -61,6 +81,7 @@ class SelectByRegexp < Base
def on_send(node)
return unless (block_node = node.block_node)
return if block_node.body.begin_type?
return if receiver_allowed?(block_node.receiver)
return unless (regexp_method_send_node = extract_send_node(block_node))

regexp = find_regexp(regexp_method_send_node)
Expand All @@ -69,6 +90,12 @@ def on_send(node)

private

def receiver_allowed?(node)
return false unless node

node.hash_type? || creates_hash?(node)
end

def register_offense(node, block_node, regexp)
replacement = REPLACEMENTS[node.method_name.to_sym]
message = format(MSG, replacement: replacement, original_method: node.method_name)
Expand Down
99 changes: 99 additions & 0 deletions spec/rubocop/cop/style/select_by_regexp_spec.rb
Expand Up @@ -124,6 +124,105 @@
RUBY
end

it 'registers an offense and corrects without a receiver' do
expect_offense(<<~RUBY, method: method)
#{method} { |x| x.match?(/regexp/) }
^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
RUBY

expect_correction(<<~RUBY)
#{correction}(/regexp/)
RUBY
end

it 'registers an offense and corrects when the receiver is an array' do
expect_offense(<<~RUBY, method: method)
[].#{method} { |x| x.match?(/regexp/) }
^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
foo.to_a.#{method} { |x| x.match?(/regexp/) }
^^^^^^^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
RUBY

expect_correction(<<~RUBY)
[].#{correction}(/regexp/)
foo.to_a.#{correction}(/regexp/)
RUBY
end

it 'registers an offense and corrects when the receiver is a range' do
expect_offense(<<~RUBY, method: method)
('aaa'...'abc').#{method} { |x| x.match?(/ab/) }
^^^^^^^^^^^^^^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^ #{message}
RUBY

expect_correction(<<~RUBY)
('aaa'...'abc').#{correction}(/ab/)
RUBY
end

it 'registers an offense and corrects when the receiver is a set' do
expect_offense(<<~RUBY, method: method)
Set.new.#{method} { |x| x.match?(/regexp/) }
^^^^^^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
[].to_set.#{method} { |x| x.match?(/regexp/) }
^^^^^^^^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
RUBY

expect_correction(<<~RUBY)
Set.new.#{correction}(/regexp/)
[].to_set.#{correction}(/regexp/)
RUBY
end

it 'does not register an offense when the receiver is a hash literal' do
expect_no_offenses(<<~RUBY)
{}.#{method} { |x| x.match? /regexp/ }
{ foo: :bar }.#{method} { |x| x.match? /regexp/ }
RUBY
end

it 'does not register an offense when the receiver is `Hash.new`' do
expect_no_offenses(<<~RUBY)
Hash.new.#{method} { |x| x.match? /regexp/ }
Hash.new(:default).#{method} { |x| x.match? /regexp/ }
Hash.new { |hash, key| :default }.#{method} { |x| x.match? /regexp/ }
RUBY
end

it 'does not register an offense when the receiver is `Hash[]`' do
expect_no_offenses(<<~RUBY)
Hash[h].#{method} { |x| x.match? /regexp/ }
Hash[:foo, 0, :bar, 1].#{method} { |x| x.match? /regexp/ }
RUBY
end

it 'does not register an offense when the receiver is `to_h`' do
expect_no_offenses(<<~RUBY)
to_h.#{method} { |x| x.match? /regexp/ }
foo.to_h.#{method} { |x| x.match? /regexp/ }
RUBY
end

it 'does not register an offense when the receiver is `to_hash`' do
expect_no_offenses(<<~RUBY)
to_hash.#{method} { |x| x.match? /regexp/ }
foo.to_hash.#{method} { |x| x.match? /regexp/ }
RUBY
end

it 'registers an offense if `to_h` is in the receiver chain but not the actual receiver' do
# Although there is a `to_h` in the chain, we cannot be sure
# of the type of the ultimate receiver.
expect_offense(<<~RUBY, method: method)
foo.to_h.bar.#{method} { |x| x.match? /regexp/ }
^^^^^^^^^^^^^^{method}^^^^^^^^^^^^^^^^^^^^^^^^^^ #{message}
RUBY

expect_correction(<<~RUBY)
foo.to_h.bar.#{correction}(/regexp/)
RUBY
end

context 'with `numblock`s', :ruby27 do
it 'registers an offense and corrects for `match?`' do
expect_offense(<<~RUBY, method: method)
Expand Down