Skip to content

Commit

Permalink
[Fix #178] Add new Performance/SelectMap cop
Browse files Browse the repository at this point in the history
Fixes #178 and #193.

This PR adds new `Performance/SelectMap` cop.

In Ruby 2.7, `Enumerable#filter_map` has been added.

This cop identifies places where `select.map` can be replaced by `filter_map`.

The following is good and bad cases.

```ruby
# bad
ary.select(&:foo).map(&:bar)
ary.filter(&:foo).map(&:bar)

# good
ary.filter_map { |o| o.bar if o.foo }
```
  • Loading branch information
koic committed Apr 12, 2021
1 parent cae4247 commit f31af62
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## master (unreleased)

### New features

* [#178](https://github.com/rubocop/rubocop-performance/issues/178): Add new `Performance/SelectMap` cop. ([@koic][])

### Changes

* [#228](https://github.com/rubocop/rubocop-performance/pull/228): Mark `Performance/RedundantMerge` as unsafe. ([@dvandersluis][])
Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ Performance/ReverseFirst:
Enabled: 'pending'
VersionAdded: '1.7'

Performance/SelectMap:
Description: 'Use `filter_map` instead of `ary.select(&:foo).map(&:bar)`.'
Enabled: pending
VersionAdded: '1.11'

Performance/Size:
Description: >-
Use `size` instead of `count` for counting
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Performance cops optimization analysis for your projects.
* xref:cops_performance.adoc#performanceregexpmatch[Performance/RegexpMatch]
* xref:cops_performance.adoc#performancereverseeach[Performance/ReverseEach]
* xref:cops_performance.adoc#performancereversefirst[Performance/ReverseFirst]
* xref:cops_performance.adoc#performanceselectmap[Performance/SelectMap]
* xref:cops_performance.adoc#performancesize[Performance/Size]
* xref:cops_performance.adoc#performancesortreverse[Performance/SortReverse]
* xref:cops_performance.adoc#performancesqueeze[Performance/Squeeze]
Expand Down
30 changes: 30 additions & 0 deletions docs/modules/ROOT/pages/cops_performance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,36 @@ array.last(5).reverse
array.last
----

== Performance/SelectMap

NOTE: Required Ruby version: 2.7

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| Yes
| No
| 1.11
| -
|===

In Ruby 2.7, `Enumerable#filter_map` has been added.

This cop identifies places where `select.map` can be replaced by `filter_map`.

=== Examples

[source,ruby]
----
# bad
ary.select(&:foo).map(&:bar)
ary.filter(&:foo).map(&:bar)
# good
ary.filter_map { |o| o.bar if o.foo }
----

== Performance/Size

|===
Expand Down
60 changes: 60 additions & 0 deletions lib/rubocop/cop/performance/select_map.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Performance
# In Ruby 2.7, `Enumerable#filter_map` has been added.
#
# This cop identifies places where `select.map` can be replaced by `filter_map`.
#
# @example
# # bad
# ary.select(&:foo).map(&:bar)
# ary.filter(&:foo).map(&:bar)
#
# # good
# ary.filter_map { |o| o.bar if o.foo }
#
class SelectMap < Base
include RangeHelp
extend TargetRubyVersion

minimum_target_ruby_version 2.7

MSG = 'Use `filter_map` instead of `%<method_name>s.map`.'
RESTRICT_ON_SEND = %i[select filter].freeze

def_node_matcher :bad_method?, <<~PATTERN
(send nil? :bad_method ...)
PATTERN

def on_send(node)
return if (first_argument = node.first_argument) && !first_argument.block_pass_type?
return unless (send_node = map_method_candidate(node))
return unless send_node.method?(:map)

map_method = send_node.parent&.block_type? ? send_node.parent : send_node

range = offense_range(node, map_method)
add_offense(range, message: format(MSG, method_name: node.method_name))
end

private

def map_method_candidate(node)
return unless (parent = node.parent)

if parent.block_type? && parent.parent&.send_type?
parent.parent
elsif parent.send_type?
parent
end
end

def offense_range(node, map_method)
range_between(node.loc.selector.begin_pos, map_method.loc.expression.end_pos)
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/performance_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
require_relative 'performance/regexp_match'
require_relative 'performance/reverse_each'
require_relative 'performance/reverse_first'
require_relative 'performance/select_map'
require_relative 'performance/size'
require_relative 'performance/sort_reverse'
require_relative 'performance/squeeze'
Expand Down
112 changes: 112 additions & 0 deletions spec/rubocop/cop/performance/select_map_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Performance::SelectMap, :config do
context 'TargetRubyVersion >= 2.7', :ruby27 do
it 'registers an offense when using `select(&:...).map(&:...)`' do
expect_offense(<<~RUBY)
ary.select(&:present?).map(&:to_i)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'registers an offense when using `filter(&:...).map(&:...)`' do
expect_offense(<<~RUBY)
ary.filter(&:present?).map(&:to_i)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `filter.map`.
RUBY
end

it 'registers an offense when using `select { ... }.map { ... }`' do
expect_offense(<<~RUBY)
ary.select { |o| o.present? }.map { |o| o.to_i }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'registers an offense when using `select { ... }.map(&:...)`' do
expect_offense(<<~RUBY)
ary.select { |o| o.present? }.map(&:to_i)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'registers an offense when using `select(&:...).map { ... }`' do
expect_offense(<<~RUBY)
ary.select(&:present?).map { |o| o.to_i }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'registers an offense when using `select(&:...).map(&:...)` in method chain' do
expect_offense(<<~RUBY)
ary.do_something.select(&:present?).map(&:to_i).max
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'registers an offense when using `select(&:...).map(&:...)` without receiver' do
expect_offense(<<~RUBY)
select(&:present?).map(&:to_i)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead of `select.map`.
RUBY
end

it 'does not register an offense when using `filter_map`' do
expect_no_offenses(<<~RUBY)
ary.filter_map { |o| o.to_i if o.present? }
RUBY
end

it 'does not register an offense when using `select`' do
expect_no_offenses(<<~RUBY)
ary.select(&:present?)
RUBY
end

it 'does not register an offense when using `select(key: value).map`' do
expect_no_offenses(<<~RUBY)
ary.do_something.select(key: value).map(&:to_i)
RUBY
end

it 'does not register an offense when using `select` with assignment' do
expect_no_offenses(<<~RUBY)
ret = select
RUBY
end

it 'does not register an offense when using `select(&:...).stranger.map(&:...)`' do
expect_no_offenses(<<~RUBY)
ary.do_something.select(&:present?).stranger.map(&:to_i).max
RUBY
end

it 'does not register an offense when using `select { ... }.stranger.map { ... }`' do
expect_no_offenses(<<~RUBY)
ary.select { |o| o.present? }.stranger.map { |o| o.to_i }
RUBY
end

#
# `compact` is not compatible with `filter_map`.
#
# [true, false, nil].compact #=> [true, false]
# [true, false, nil].filter_map(&:itself) #=> [true]
#
# Use `Performance/MapCompact` cop.
#
it 'does not register an offense when using `map(&:...).compact`' do
expect_no_offenses(<<~RUBY)
ary.map(&:to_i).compact
RUBY
end
end

context 'TargetRubyVersion <= 2.6', :ruby26 do
it 'does not register an offense when using `select.map`' do
expect_no_offenses(<<~RUBY)
ary.select(&:present?).map(&:to_i)
RUBY
end
end
end

0 comments on commit f31af62

Please sign in to comment.