Skip to content

Commit

Permalink
Add Performance/MapCompact cop
Browse files Browse the repository at this point in the history
Follow #211 (comment)

This PR adds `Performance/MapCompact` cop

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

This cop identifies places where `map { ... }.compact` can be replaced by `filter_map`.
It is marked as unsafe auto-correction by default because `map { ... }.compact`
that is not compatible with `filter_map`.

The following is good and bad cases.

```ruby
# bad
ary.map(&:foo).compact
ary.collect(&:foo).compact

# good
ary.filter_map(&:foo)
ary.map(&:foo).compact!
```
  • Loading branch information
koic committed Mar 29, 2021
1 parent f7de801 commit 970d0fb
Show file tree
Hide file tree
Showing 7 changed files with 215 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

* [#229](https://github.com/rubocop/rubocop-performance/pull/229): Add new `Add `Performance/MapCompact` cop` cop. ([@koic][])

### Changes

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

Performance/MapCompact:
Description: 'Use `filter_map` instead of `collection.map(&:do_something).compact`.'
Enabled: pending
SafeAutoCorrect: false
VersionAdded: '1.11'

Performance/MethodObjectAsBlock:
Description: 'Use block explicitly instead of block-passing a method object.'
Reference: 'https://github.com/JuanitoFatas/fast-ruby#normal-way-to-apply-method-vs-method-code'
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 @@ -34,6 +34,7 @@ Performance cops optimization analysis for your projects.
* xref:cops_performance.adoc#performanceflatmap[Performance/FlatMap]
* xref:cops_performance.adoc#performanceinefficienthashsearch[Performance/InefficientHashSearch]
* xref:cops_performance.adoc#performanceioreadlines[Performance/IoReadlines]
* xref:cops_performance.adoc#performancemapcompact[Performance/MapCompact]
* xref:cops_performance.adoc#performancemethodobjectasblock[Performance/MethodObjectAsBlock]
* xref:cops_performance.adoc#performanceopenstruct[Performance/OpenStruct]
* xref:cops_performance.adoc#performancerangeinclude[Performance/RangeInclude]
Expand Down
39 changes: 39 additions & 0 deletions docs/modules/ROOT/pages/cops_performance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,45 @@ file.each_line { |l| puts l }

* https://docs.gitlab.com/ee/development/performance.html#reading-from-files-and-other-data-sources

== Performance/MapCompact

NOTE: Required Ruby version: 2.7

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

| Pending
| Yes
| Yes (Unsafe)
| 1.11
| -
|===

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

This cop identifies places where `map { ... }.compact` can be replaced by `filter_map`.
It is marked as unsafe auto-correction by default because `map { ... }.compact`
that is not compatible with `filter_map`.

[source,ruby]
----
[true, false, nil].compact #=> [true, false]
[true, false, nil].filter_map(&:itself) #=> [true]
----

=== Examples

[source,ruby]
----
# bad
ary.map(&:foo).compact
ary.collect(&:foo).compact
# good
ary.filter_map(&:foo)
ary.map(&:foo).compact!
----

== Performance/MethodObjectAsBlock

|===
Expand Down
65 changes: 65 additions & 0 deletions lib/rubocop/cop/performance/map_compact.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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 `map { ... }.compact` can be replaced by `filter_map`.
# It is marked as unsafe auto-correction by default because `map { ... }.compact`
# that is not compatible with `filter_map`.
#
# [source,ruby]
# ----
# [true, false, nil].compact #=> [true, false]
# [true, false, nil].filter_map(&:itself) #=> [true]
# ----
#
# @example
# # bad
# ary.map(&:foo).compact
# ary.collect(&:foo).compact
#
# # good
# ary.filter_map(&:foo)
# ary.map(&:foo).compact!
#
class MapCompact < Base
include RangeHelp
extend AutoCorrector
extend TargetRubyVersion

MSG = 'Use `filter_map` instead.'
RESTRICT_ON_SEND = %i[compact].freeze

minimum_target_ruby_version 2.7

def_node_matcher :map_compact?, <<~PATTERN
{
(send
$(send _ {:map :collect}
(block_pass
(sym _))) _)
(send
(block
$(send _ {:map :collect})
(args ...) _) _)
}
PATTERN

def on_send(node)
return unless (map_node = map_compact?(node))

compact_loc = node.loc
range = range_between(map_node.loc.selector.begin_pos, compact_loc.selector.end_pos)

add_offense(range) do |corrector|
corrector.replace(map_node.loc.selector, 'filter_map')
corrector.remove(compact_loc.dot)
corrector.remove(compact_loc.selector)
end
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 @@ -23,6 +23,7 @@
require_relative 'performance/fixed_size'
require_relative 'performance/flat_map'
require_relative 'performance/inefficient_hash_search'
require_relative 'performance/map_compact'
require_relative 'performance/method_object_as_block'
require_relative 'performance/open_struct'
require_relative 'performance/range_include'
Expand Down
99 changes: 99 additions & 0 deletions spec/rubocop/cop/performance/map_compact_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Performance::MapCompact, :config do
context 'TargetRubyVersion >= 2.7', :ruby27 do
it 'registers an offense when using `collection.map(&:do_something).compact`' do
expect_offense(<<~RUBY)
collection.map(&:do_something).compact
^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead.
RUBY

expect_correction(<<~RUBY)
collection.filter_map(&:do_something)
RUBY
end

it 'registers an offense when using `collection.map { |item| item.do_something }.compact`' do
expect_offense(<<~RUBY)
collection.map { |item| item.do_something }.compact
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead.
RUBY

expect_correction(<<~RUBY)
collection.filter_map { |item| item.do_something }
RUBY
end

it 'registers an offense when using `collection.collect(&:do_something).compact`' do
expect_offense(<<~RUBY)
collection.collect(&:do_something).compact
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead.
RUBY

expect_correction(<<~RUBY)
collection.filter_map(&:do_something)
RUBY
end

it 'registers an offense when using `collection.collect { |item| item.do_something }.compact`' do
expect_offense(<<~RUBY)
collection.collect { |item| item.do_something }.compact
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `filter_map` instead.
RUBY

expect_correction(<<~RUBY)
collection.filter_map { |item| item.do_something }
RUBY
end

it 'does not register an offense when using `collection.map(&:do_something).compact!`' do
expect_no_offenses(<<~RUBY)
collection.map(&:do_something).compact!
RUBY
end

it 'does not register an offense when using `collection.map { |item| item.do_something }.compact!`' do
expect_no_offenses(<<~RUBY)
collection.map { |item| item.do_something }.compact!
RUBY
end

it 'does not register an offense when using `collection.collect(&:do_something).compact!`' do
expect_no_offenses(<<~RUBY)
collection.collect(&:do_something).compact!
RUBY
end

it 'does not register an offense when using `collection.collect { |item| item.do_something }.compact!`' do
expect_no_offenses(<<~RUBY)
collection.collect { |item| item.do_something }.compact!
RUBY
end

it 'does not register an offense when using `collection.not_map_method(&:do_something).compact`' do
expect_no_offenses(<<~RUBY)
collection.not_map_method(&:do_something).compact
RUBY
end

it 'does not register an offense when using `collection.filter_map(&:do_something)`' do
expect_no_offenses(<<~RUBY)
collection.filter_map(&:do_something)
RUBY
end
end

context 'TargetRubyVersion <= 2.6', :ruby26 do
it 'does not register an offense when using `collection.map(&:do_something).compact`' do
expect_no_offenses(<<~RUBY)
collection.map(&:do_something).compact
RUBY
end

it 'does not register an offense when using `collection.map { |item| item.do_something }.compact`' do
expect_no_offenses(<<~RUBY)
collection.map { |item| item.do_something }.compact
RUBY
end
end
end

0 comments on commit 970d0fb

Please sign in to comment.