diff --git a/CHANGELOG.md b/CHANGELOG.md index 524175e326..8f231548f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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][]) diff --git a/config/default.yml b/config/default.yml index 8f4feb984a..bafd282f34 100644 --- a/config/default.yml +++ b/config/default.yml @@ -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' diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 01a1900d24..234a3da0fd 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -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] diff --git a/docs/modules/ROOT/pages/cops_performance.adoc b/docs/modules/ROOT/pages/cops_performance.adoc index 0f0aa671ea..112dc7615f 100644 --- a/docs/modules/ROOT/pages/cops_performance.adoc +++ b/docs/modules/ROOT/pages/cops_performance.adoc @@ -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 |=== diff --git a/lib/rubocop/cop/performance/map_compact.rb b/lib/rubocop/cop/performance/map_compact.rb new file mode 100644 index 0000000000..b9fb381540 --- /dev/null +++ b/lib/rubocop/cop/performance/map_compact.rb @@ -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 diff --git a/lib/rubocop/cop/performance_cops.rb b/lib/rubocop/cop/performance_cops.rb index 41ca77a3c3..9725e25f70 100644 --- a/lib/rubocop/cop/performance_cops.rb +++ b/lib/rubocop/cop/performance_cops.rb @@ -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' diff --git a/spec/rubocop/cop/performance/map_compact_spec.rb b/spec/rubocop/cop/performance/map_compact_spec.rb new file mode 100644 index 0000000000..67b551a5a5 --- /dev/null +++ b/spec/rubocop/cop/performance/map_compact_spec.rb @@ -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