From 785c7ecd691903f4c2df5027fcd3b280279feb50 Mon Sep 17 00:00:00 2001 From: r7kamura Date: Sat, 27 Jan 2024 11:58:58 +0900 Subject: [PATCH] Add `Sevencop/MapMethodChain` cop --- README.md | 1 + config/default.yml | 6 ++ lib/rubocop/cop/sevencop/map_method_chain.rb | 74 +++++++++++++++++++ lib/sevencop.rb | 1 + .../cop/sevencop/map_method_chain_spec.rb | 29 ++++++++ 5 files changed, 111 insertions(+) create mode 100644 lib/rubocop/cop/sevencop/map_method_chain.rb create mode 100644 spec/rubocop/cop/sevencop/map_method_chain_spec.rb diff --git a/README.md b/README.md index a141545..dd665da 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Note that all cops are `Enabled: false` by default. - [Sevencop/FactoryBotAssociationOption](lib/rubocop/cop/sevencop/factory_bot_association_option.rb) - [Sevencop/FactoryBotAssociationStyle](lib/rubocop/cop/sevencop/factory_bot_association_style.rb) - [Sevencop/HashElementOrdered](lib/rubocop/cop/sevencop/hash_element_ordered.rb) +- [Sevencop/MapMethodChain](lib/rubocop/cop/sevencop/map_method_chain.rb) - [Sevencop/MethodDefinitionArgumentsMultiline](lib/rubocop/cop/sevencop/method_definition_arguments_multiline.rb) - [Sevencop/MethodDefinitionInIncluded](lib/rubocop/cop/sevencop/method_definition_in_included.rb) - [Sevencop/MethodDefinitionKeywordArgumentOrdered](lib/rubocop/cop/sevencop/method_definition_keyword_argument_ordered.rb) diff --git a/config/default.yml b/config/default.yml index 1bd0ba8..61581e4 100644 --- a/config/default.yml +++ b/config/default.yml @@ -37,6 +37,12 @@ Sevencop/HashElementOrdered: Enabled: false Safe: false +Sevencop/MapMethodChain: + Description: | + Checks if the map method is used in a chain. + Enabled: false + Safe: false + Sevencop/MethodDefinitionArgumentsMultiline: Description: | Inserts new lines between method definition arguments. diff --git a/lib/rubocop/cop/sevencop/map_method_chain.rb b/lib/rubocop/cop/sevencop/map_method_chain.rb new file mode 100644 index 0000000..5711ddf --- /dev/null +++ b/lib/rubocop/cop/sevencop/map_method_chain.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Sevencop + # Checks if the map method is used in a chain. + # + # This cop is another version of `Performance/MapMethodChain` cop which has the autocorrection support. + # They have decided not to add autocorrection, so we have this cop in case you want to use it. + # https://github.com/rubocop/rubocop-performance/issues/436 + # + # @example + # # bad + # array.map(&:foo).map(&:bar) + # + # # good + # array.map { |element| element.foo.bar } + class MapMethodChain < Base + extend AutoCorrector + + include IgnoredNode + + RESTRICT_ON_SEND = %i[map collect].freeze + + # @!method block_pass_with_symbol_arg?(node) + def_node_matcher :block_pass_with_symbol_arg?, <<~PATTERN + (:block_pass (:sym $_)) + PATTERN + + def on_send(node) + return if part_of_ignored_node?(node) + return unless (map_arg = block_pass_with_symbol_arg?(node.first_argument)) + + map_args = [map_arg] + return unless (begin_of_chained_map_method = find_begin_of_chained_map_method(node, map_args)) + + range = begin_of_chained_map_method.loc.selector.begin.join(node.source_range.end) + replacement = "#{begin_of_chained_map_method.method_name} { |element| element.#{map_args.join('.')} }" + add_offense( + range, + message: format( + 'Use `%s` instead of `%s` method chain.', + method_name: begin_of_chained_map_method.method_name, + replacement: replacement + ) + ) do |corrector| + corrector.replace(range, replacement) + end + + ignore_node(node) + end + + private + + def find_begin_of_chained_map_method( + node, + map_args + ) + return unless (chained_map_method = node.receiver) + return if !chained_map_method.call_type? || !RESTRICT_ON_SEND.include?(chained_map_method.method_name) + return unless (map_arg = block_pass_with_symbol_arg?(chained_map_method.first_argument)) + + map_args.unshift(map_arg) + + receiver = chained_map_method.receiver + + return chained_map_method unless receiver&.call_type? && block_pass_with_symbol_arg?(receiver.first_argument) + + find_begin_of_chained_map_method(chained_map_method, map_args) + end + end + end + end +end diff --git a/lib/sevencop.rb b/lib/sevencop.rb index c4aa8bb..112f0f8 100644 --- a/lib/sevencop.rb +++ b/lib/sevencop.rb @@ -8,6 +8,7 @@ require_relative 'rubocop/cop/sevencop/factory_bot_association_option' require_relative 'rubocop/cop/sevencop/factory_bot_association_style' require_relative 'rubocop/cop/sevencop/hash_element_ordered' +require_relative 'rubocop/cop/sevencop/map_method_chain' require_relative 'rubocop/cop/sevencop/method_definition_arguments_multiline' require_relative 'rubocop/cop/sevencop/method_definition_in_included' require_relative 'rubocop/cop/sevencop/method_definition_keyword_argument_ordered' diff --git a/spec/rubocop/cop/sevencop/map_method_chain_spec.rb b/spec/rubocop/cop/sevencop/map_method_chain_spec.rb new file mode 100644 index 0000000..44702f0 --- /dev/null +++ b/spec/rubocop/cop/sevencop/map_method_chain_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::Sevencop::MapMethodChain, :config do + context 'with 2 map method chain' do + it 'registers offense' do + expect_offense(<<~RUBY) + array.map(&:foo).map(&:bar) + ^^^^^^^^^^^^^^^^^^^^^ Use `map { |element| element.foo.bar }` instead of `map` method chain. + RUBY + + expect_correction(<<~RUBY) + array.map { |element| element.foo.bar } + RUBY + end + end + + context 'with 3 map method chain' do + it 'registers offense' do + expect_offense(<<~RUBY) + array&.map(&:foo).map(&:bar).map(&:baz) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `map { |element| element.foo.bar.baz }` instead of `map` method chain. + RUBY + + expect_correction(<<~RUBY) + array&.map { |element| element.foo.bar.baz } + RUBY + end + end +end