Skip to content

Commit 6b4d243

Browse files
authored
Merge pull request #364 from koic/add_new_performance_map_method_chain_cop
Add new `Performance/MapMethodChain` cop
2 parents d7a141d + 42c78bc commit 6b4d243

File tree

5 files changed

+173
-0
lines changed

5 files changed

+173
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#364](https://github.com/rubocop/rubocop-performance/pull/364): Add new `Performance/MapMethodChain` cop. ([@koic][])

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ Performance/MapCompact:
193193
SafeAutoCorrect: false
194194
VersionAdded: '1.11'
195195

196+
Performance/MapMethodChain:
197+
Description: 'Checks if the `map` method is used in a chain.'
198+
Enabled: pending
199+
Safe: false
200+
VersionAdded: '<<next>>'
201+
196202
Performance/MethodObjectAsBlock:
197203
Description: 'Use block explicitly instead of block-passing a method object.'
198204
Reference: 'https://github.com/JuanitoFatas/fast-ruby#normal-way-to-apply-method-vs-method-code'
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Performance
6+
# Checks if the map method is used in a chain.
7+
#
8+
# Autocorrection is not supported because an appropriate block variable name cannot be determined automatically.
9+
#
10+
# @safety
11+
# This cop is unsafe because false positives occur if the number of times the first method is executed
12+
# affects the return value of subsequent methods.
13+
#
14+
# [source,ruby]
15+
# ----
16+
# class X
17+
# def initialize
18+
# @@num = 0
19+
# end
20+
#
21+
# def foo
22+
# @@num += 1
23+
# self
24+
# end
25+
#
26+
# def bar
27+
# @@num * 2
28+
# end
29+
# end
30+
#
31+
# [X.new, X.new].map(&:foo).map(&:bar) # => [4, 4]
32+
# [X.new, X.new].map { |x| x.foo.bar } # => [2, 4]
33+
# ----
34+
#
35+
# @example
36+
#
37+
# # bad
38+
# array.map(&:foo).map(&:bar)
39+
#
40+
# # good
41+
# array.map { |item| item.foo.bar }
42+
#
43+
class MapMethodChain < Base
44+
include IgnoredNode
45+
46+
MSG = 'Use `%<method_name>s { |x| x.%<map_args>s }` instead of `%<method_name>s` method chain.'
47+
RESTRICT_ON_SEND = %i[map collect].freeze
48+
49+
def_node_matcher :block_pass_with_symbol_arg?, <<~PATTERN
50+
(:block_pass (:sym $_))
51+
PATTERN
52+
53+
def on_send(node)
54+
return if part_of_ignored_node?(node)
55+
return unless (map_arg = block_pass_with_symbol_arg?(node.first_argument))
56+
57+
map_args = [map_arg]
58+
59+
return unless (begin_of_chained_map_method = find_begin_of_chained_map_method(node, map_args))
60+
61+
range = begin_of_chained_map_method.loc.selector.begin.join(node.source_range.end)
62+
message = format(MSG, method_name: begin_of_chained_map_method.method_name, map_args: map_args.join('.'))
63+
64+
add_offense(range, message: message)
65+
66+
ignore_node(node)
67+
end
68+
69+
private
70+
71+
def find_begin_of_chained_map_method(node, map_args)
72+
return unless (chained_map_method = node.receiver)
73+
return if !chained_map_method.call_type? || !RESTRICT_ON_SEND.include?(chained_map_method.method_name)
74+
return unless (map_arg = block_pass_with_symbol_arg?(chained_map_method.first_argument))
75+
76+
map_args.unshift(map_arg)
77+
78+
receiver = chained_map_method.receiver
79+
80+
return chained_map_method unless receiver.call_type? && block_pass_with_symbol_arg?(receiver.first_argument)
81+
82+
find_begin_of_chained_map_method(chained_map_method, map_args)
83+
end
84+
end
85+
end
86+
end
87+
end

lib/rubocop/cop/performance_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require_relative 'performance/flat_map'
2626
require_relative 'performance/inefficient_hash_search'
2727
require_relative 'performance/map_compact'
28+
require_relative 'performance/map_method_chain'
2829
require_relative 'performance/method_object_as_block'
2930
require_relative 'performance/open_struct'
3031
require_relative 'performance/range_include'
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Performance::MapMethodChain, :config do
4+
it 'registers an offense when using `map` method chain and receiver is a method call' do
5+
expect_offense(<<~RUBY)
6+
array.map(&:foo).map(&:bar)
7+
^^^^^^^^^^^^^^^^^^^^^ Use `map { |x| x.foo.bar }` instead of `map` method chain.
8+
RUBY
9+
end
10+
11+
it 'registers an offense when using `collect` method chain and receiver is a method call' do
12+
expect_offense(<<~RUBY)
13+
array.collect(&:foo).collect(&:bar)
14+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `collect { |x| x.foo.bar }` instead of `collect` method chain.
15+
RUBY
16+
end
17+
18+
it 'registers an offense when using `map` and `collect` method chain and receiver is a method call' do
19+
expect_offense(<<~RUBY)
20+
array.map(&:foo).collect(&:bar)
21+
^^^^^^^^^^^^^^^^^^^^^^^^^ Use `map { |x| x.foo.bar }` instead of `map` method chain.
22+
RUBY
23+
end
24+
25+
it 'registers an offense when using `map` method chain and receiver is a variable' do
26+
expect_offense(<<~RUBY)
27+
array = create_array
28+
array.map(&:foo).map(&:bar)
29+
^^^^^^^^^^^^^^^^^^^^^ Use `map { |x| x.foo.bar }` instead of `map` method chain.
30+
RUBY
31+
end
32+
33+
it 'registers an offense when using `map` method chain repeated three times' do
34+
expect_offense(<<~RUBY)
35+
array.map(&:foo).map(&:bar).map(&:baz)
36+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `map { |x| x.foo.bar.baz }` instead of `map` method chain.
37+
RUBY
38+
end
39+
40+
it 'registers an offense when using `map` method chain repeated three times with safe navigation' do
41+
expect_offense(<<~RUBY)
42+
array&.map(&:foo).map(&:bar).map(&:baz)
43+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `map { |x| x.foo.bar.baz }` instead of `map` method chain.
44+
RUBY
45+
end
46+
47+
it 'does not register an offense when using `do_something` method chain and receiver is a method call' do
48+
expect_no_offenses(<<~RUBY)
49+
array.do_something(&:foo).do_something(&:bar)
50+
RUBY
51+
end
52+
53+
it 'does not register an offense when there is a method call between `map` method chain' do
54+
expect_no_offenses(<<~RUBY)
55+
array.map(&:foo).do_something.map(&:bar)
56+
RUBY
57+
end
58+
59+
it 'does not register an offense when using `flat_map` and `map` method chain and receiver is a method call' do
60+
expect_no_offenses(<<~RUBY)
61+
array.flat_map(&:foo).map(&:bar)
62+
RUBY
63+
end
64+
65+
it 'does not register an offense when using `map(&:foo).join(', ')`' do
66+
expect_no_offenses(<<~RUBY)
67+
array = create_array
68+
array.map(&:foo).join(', ')
69+
RUBY
70+
end
71+
72+
it 'does not register an offense when using `map(&:foo).join(', ')` with safe navigation' do
73+
expect_no_offenses(<<~RUBY)
74+
array = create_array
75+
array.map(&:foo).join(', ')
76+
RUBY
77+
end
78+
end

0 commit comments

Comments
 (0)