Skip to content

Commit 1bcba4d

Browse files
authored
Merge pull request #1102 from koic/add_new_rails_select_map_cop
[Fix #1075] Add new `Rails/SelectMap` cop
2 parents 38e408a + 5043655 commit 1bcba4d

File tree

5 files changed

+172
-0
lines changed

5 files changed

+172
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#1075](https://github.com/rubocop/rubocop-rails/issues/1075): Add new `Rails/SelectMap` cop that checks for uses of `select(:column_name)` with `map(&:column_name)`. ([@koic][])

config/default.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,12 @@ Rails/ScopeArgs:
983983
Include:
984984
- app/models/**/*.rb
985985

986+
Rails/SelectMap:
987+
Description: 'Checks for uses of `select(:column_name)` with `map(&:column_name)`.'
988+
Enabled: pending
989+
Safe: false
990+
VersionAdded: '<<next>>'
991+
986992
Rails/ShortI18n:
987993
Description: 'Use the short form of the I18n methods: `t` instead of `translate` and `l` instead of `localize`.'
988994
StyleGuide: 'https://rails.rubystyle.guide/#short-i18n'

lib/rubocop/cop/rails/select_map.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# Checks for uses of `select(:column_name)` with `map(&:column_name)`.
7+
# These can be replaced with `pluck(:column_name)`.
8+
#
9+
# There also should be some performance improvement since it skips instantiating the model class for matches.
10+
#
11+
# @safety
12+
# This cop is unsafe because the model might override the attribute getter.
13+
# Additionally, the model's `after_initialize` hooks are skipped when using `pluck`.
14+
#
15+
# @example
16+
# # bad
17+
# Model.select(:column_name).map(&:column_name)
18+
#
19+
# # good
20+
# Model.pluck(:column_name)
21+
#
22+
class SelectMap < Base
23+
extend AutoCorrector
24+
25+
MSG = 'Use `%<preferred_method>s` instead of `select` with `%<map_method>s`.'
26+
27+
RESTRICT_ON_SEND = %i[map collect].freeze
28+
29+
def on_send(node)
30+
return unless node.first_argument
31+
32+
column_name = node.first_argument.source.delete_prefix('&:')
33+
return unless (select_node = find_select_node(node, column_name))
34+
35+
offense_range = select_node.loc.selector.begin.join(node.source_range.end)
36+
preferred_method = "pluck(:#{column_name})"
37+
message = format(MSG, preferred_method: preferred_method, map_method: node.method_name)
38+
39+
add_offense(offense_range, message: message) do |corrector|
40+
autocorrect(corrector, select_node, node, preferred_method)
41+
end
42+
end
43+
44+
private
45+
46+
def find_select_node(node, column_name)
47+
node.descendants.detect do |select_candidate|
48+
next if !select_candidate.send_type? || !select_candidate.method?(:select)
49+
50+
match_column_name?(select_candidate, column_name)
51+
end
52+
end
53+
54+
def autocorrect(corrector, select_node, node, preferred_method)
55+
corrector.remove(select_node.loc.dot.begin.join(select_node.source_range.end))
56+
corrector.replace(node.loc.selector.begin.join(node.source_range.end), preferred_method)
57+
end
58+
59+
def match_column_name?(select_candidate, column_name)
60+
return false unless select_candidate.arguments.one?
61+
return false unless (first_argument = select_candidate.first_argument)
62+
63+
argument = case select_candidate.first_argument.type
64+
when :sym
65+
first_argument.source.delete_prefix(':')
66+
when :str
67+
first_argument.value if first_argument.respond_to?(:value)
68+
end
69+
70+
argument == column_name
71+
end
72+
end
73+
end
74+
end
75+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
require_relative 'rails/save_bang'
113113
require_relative 'rails/schema_comment'
114114
require_relative 'rails/scope_args'
115+
require_relative 'rails/select_map'
115116
require_relative 'rails/short_i18n'
116117
require_relative 'rails/skips_model_validations'
117118
require_relative 'rails/squished_sql_heredocs'
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::SelectMap, :config do
4+
it 'registers an offense when using `select(:column_name).map(&:column_name)`' do
5+
expect_offense(<<~RUBY)
6+
Model.select(:column_name).map(&:column_name)
7+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `pluck(:column_name)` instead of `select` with `map`.
8+
RUBY
9+
10+
expect_correction(<<~RUBY)
11+
Model.pluck(:column_name)
12+
RUBY
13+
end
14+
15+
it "registers an offense when using `select('column_name').map(&:column_name)`" do
16+
expect_offense(<<~RUBY)
17+
Model.select('column_name').map(&:column_name)
18+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `pluck(:column_name)` instead of `select` with `map`.
19+
RUBY
20+
21+
expect_correction(<<~RUBY)
22+
Model.pluck(:column_name)
23+
RUBY
24+
end
25+
26+
it 'registers an offense when using `select(:column_name).collect(&:column_name)`' do
27+
expect_offense(<<~RUBY)
28+
Model.select(:column_name).collect(&:column_name)
29+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `pluck(:column_name)` instead of `select` with `collect`.
30+
RUBY
31+
32+
expect_correction(<<~RUBY)
33+
Model.pluck(:column_name)
34+
RUBY
35+
end
36+
37+
it 'registers an offense when using `select(:column_name).where(conditions).map(&:column_name)`' do
38+
expect_offense(<<~RUBY)
39+
Model.select(:column_name).where(conditions).map(&:column_name)
40+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `pluck(:column_name)` instead of `select` with `map`.
41+
RUBY
42+
43+
expect_correction(<<~RUBY)
44+
Model.where(conditions).pluck(:column_name)
45+
RUBY
46+
end
47+
48+
it 'does not register an offense when using `select(:mismatch_column_name).map(&:column_name)`' do
49+
expect_no_offenses(<<~RUBY)
50+
Model.select(:mismatch_column_name).map(&:column_name)
51+
RUBY
52+
end
53+
54+
it 'does not register an offense when using `select(:column_name, :other_column_name).map(&:column_name)`' do
55+
expect_no_offenses(<<~RUBY)
56+
Model.select(:column_name, :other_column_name).map(&:column_name)
57+
RUBY
58+
end
59+
60+
it 'does not register an offense when using `select(column_names).map(&:column_name)`' do
61+
expect_no_offenses(<<~RUBY)
62+
Model.select(column_names).map(&:column_name)
63+
RUBY
64+
end
65+
66+
it 'does not register an offense when using `select(:column_name).do_something(&:column_name)`' do
67+
expect_no_offenses(<<~RUBY)
68+
Model.select(:column_name).do_something(&:column_name)
69+
RUBY
70+
end
71+
72+
it 'does not register an offense when using `select { |item| item.column_name }.map(&:column_name)`' do
73+
expect_no_offenses(<<~RUBY)
74+
Model.select { |item| item.column_name }.map(&:column_name)
75+
RUBY
76+
end
77+
78+
it 'does not register an offense when using `select(:column_name).map { |item| do_something(item) }`' do
79+
expect_no_offenses(<<~RUBY)
80+
Model.select(:column_name).map { |item| do_something(item) }
81+
RUBY
82+
end
83+
84+
it 'does not register an offense when using `pluck(:column_name)`' do
85+
expect_no_offenses(<<~RUBY)
86+
Model.pluck(:column_name)
87+
RUBY
88+
end
89+
end

0 commit comments

Comments
 (0)