Skip to content

Commit 497a2aa

Browse files
authored
Merge pull request #581 from pirj/improve-uniq_before_pluck-examples
Iron out Rails/UniqBeforePluck
2 parents b2b0e1b + fca3863 commit 497a2aa

File tree

3 files changed

+133
-117
lines changed

3 files changed

+133
-117
lines changed

docs/modules/ROOT/pages/cops_rails.adoc

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4912,58 +4912,56 @@ end
49124912
| 2.13
49134913
|===
49144914

4915-
Prefer the use of distinct, before pluck instead of after.
4915+
Prefer using `distinct` before `pluck` instead of `uniq` after `pluck`.
49164916

4917-
The use of distinct before pluck is preferred because it executes within
4917+
The use of distinct before pluck is preferred because it executes by
49184918
the database.
49194919

49204920
This cop has two different enforcement modes. When the EnforcedStyle
4921-
is conservative (the default) then only calls to pluck on a constant
4922-
(i.e. a model class) before distinct are added as offenses.
4921+
is `conservative` (the default), then only calls to `pluck` on a constant
4922+
(i.e. a model class) before `uniq` are added as offenses.
49234923

4924-
When the EnforcedStyle is aggressive then all calls to pluck before
4924+
When the EnforcedStyle is `aggressive` then all calls to `pluck` before
49254925
distinct are added as offenses. This may lead to false positives
4926-
as the cop cannot distinguish between calls to pluck on an
4926+
as the cop cannot distinguish between calls to `pluck` on an
49274927
ActiveRecord::Relation vs a call to pluck on an
49284928
ActiveRecord::Associations::CollectionProxy.
49294929

49304930
=== Safety
49314931

4932-
This cop is unsafe because the behavior may change depending on the
4933-
database collation.
4934-
Autocorrect is disabled by default for this cop since it may generate
4935-
false positives.
4932+
This cop is unsafe for autocorrection because the behavior may change
4933+
depending on the database collation.
49364934

49374935
=== Examples
49384936

49394937
==== EnforcedStyle: conservative (default)
49404938

49414939
[source,ruby]
49424940
----
4943-
# bad
4944-
Model.pluck(:id).uniq
4941+
# bad - redundantly fetches duplicate values
4942+
Album.pluck(:band_name).uniq
49454943
49464944
# good
4947-
Model.distinct.pluck(:id)
4945+
Album.distinct.pluck(:band_name)
49484946
----
49494947

49504948
==== EnforcedStyle: aggressive
49514949

49524950
[source,ruby]
49534951
----
4954-
# bad
4955-
# this will return a Relation that pluck is called on
4956-
Model.where(cond: true).pluck(:id).uniq
4952+
# bad - redundantly fetches duplicate values
4953+
Album.pluck(:band_name).uniq
49574954
4958-
# bad
4959-
# an association on an instance will return a CollectionProxy
4960-
instance.assoc.pluck(:id).uniq
4955+
# bad - redundantly fetches duplicate values
4956+
Album.where(year: 1985).pluck(:band_name).uniq
49614957
4962-
# bad
4963-
Model.pluck(:id).uniq
4958+
# bad - redundantly fetches duplicate values
4959+
customer.favourites.pluck(:color).uniq
49644960
49654961
# good
4966-
Model.distinct.pluck(:id)
4962+
Album.distinct.pluck(:band_name)
4963+
Album.distinct.where(year: 1985).pluck(:band_name)
4964+
customer.favourites.distinct.pluck(:color)
49674965
----
49684966

49694967
=== Configurable attributes

lib/rubocop/cop/rails/uniq_before_pluck.rb

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,56 @@
33
module RuboCop
44
module Cop
55
module Rails
6-
# Prefer the use of distinct, before pluck instead of after.
6+
# Prefer using `distinct` before `pluck` instead of `uniq` after `pluck`.
77
#
8-
# The use of distinct before pluck is preferred because it executes within
8+
# The use of distinct before pluck is preferred because it executes by
99
# the database.
1010
#
1111
# This cop has two different enforcement modes. When the EnforcedStyle
12-
# is conservative (the default) then only calls to pluck on a constant
13-
# (i.e. a model class) before distinct are added as offenses.
12+
# is `conservative` (the default), then only calls to `pluck` on a constant
13+
# (i.e. a model class) before `uniq` are added as offenses.
1414
#
15-
# When the EnforcedStyle is aggressive then all calls to pluck before
15+
# When the EnforcedStyle is `aggressive` then all calls to `pluck` before
1616
# distinct are added as offenses. This may lead to false positives
17-
# as the cop cannot distinguish between calls to pluck on an
17+
# as the cop cannot distinguish between calls to `pluck` on an
1818
# ActiveRecord::Relation vs a call to pluck on an
1919
# ActiveRecord::Associations::CollectionProxy.
2020
#
2121
# @safety
22-
# This cop is unsafe because the behavior may change depending on the
23-
# database collation.
24-
# Autocorrect is disabled by default for this cop since it may generate
25-
# false positives.
22+
# This cop is unsafe for autocorrection because the behavior may change
23+
# depending on the database collation.
2624
#
2725
# @example EnforcedStyle: conservative (default)
28-
# # bad
29-
# Model.pluck(:id).uniq
26+
# # bad - redundantly fetches duplicate values
27+
# Album.pluck(:band_name).uniq
3028
#
3129
# # good
32-
# Model.distinct.pluck(:id)
30+
# Album.distinct.pluck(:band_name)
3331
#
3432
# @example EnforcedStyle: aggressive
35-
# # bad
36-
# # this will return a Relation that pluck is called on
37-
# Model.where(cond: true).pluck(:id).uniq
33+
# # bad - redundantly fetches duplicate values
34+
# Album.pluck(:band_name).uniq
3835
#
39-
# # bad
40-
# # an association on an instance will return a CollectionProxy
41-
# instance.assoc.pluck(:id).uniq
36+
# # bad - redundantly fetches duplicate values
37+
# Album.where(year: 1985).pluck(:band_name).uniq
4238
#
43-
# # bad
44-
# Model.pluck(:id).uniq
39+
# # bad - redundantly fetches duplicate values
40+
# customer.favourites.pluck(:color).uniq
4541
#
4642
# # good
47-
# Model.distinct.pluck(:id)
43+
# Album.distinct.pluck(:band_name)
44+
# Album.distinct.where(year: 1985).pluck(:band_name)
45+
# customer.favourites.distinct.pluck(:color)
4846
#
4947
class UniqBeforePluck < Base
5048
include ConfigurableEnforcedStyle
5149
include RangeHelp
5250
extend AutoCorrector
5351

5452
MSG = 'Use `distinct` before `pluck`.'
55-
RESTRICT_ON_SEND = %i[uniq distinct pluck].freeze
53+
RESTRICT_ON_SEND = %i[uniq].freeze
5654
NEWLINE = "\n"
57-
PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
58-
'${:uniq :distinct} ...)]'
55+
PATTERN = '[!^block (send (send %<type>s :pluck ...) :uniq ...)]'
5956

6057
def_node_matcher :conservative_node_match,
6158
format(PATTERN, type: 'const')
@@ -64,13 +61,13 @@ class UniqBeforePluck < Base
6461
format(PATTERN, type: '_')
6562

6663
def on_send(node)
67-
method = if style == :conservative
68-
conservative_node_match(node)
69-
else
70-
aggressive_node_match(node)
71-
end
64+
uniq = if style == :conservative
65+
conservative_node_match(node)
66+
else
67+
aggressive_node_match(node)
68+
end
7269

73-
return unless method
70+
return unless uniq
7471

7572
add_offense(node.loc.selector) do |corrector|
7673
method = node.method_name
@@ -82,10 +79,6 @@ def on_send(node)
8279

8380
private
8481

85-
def style_parameter_name
86-
'EnforcedStyle'
87-
end
88-
8982
def dot_method_with_whitespace(method, node)
9083
range_between(dot_method_begin_pos(method, node),
9184
node.loc.selector.end_pos)
Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,117 @@
11
# frozen_string_literal: true
22

33
RSpec.describe RuboCop::Cop::Rails::UniqBeforePluck, :config do
4-
shared_examples_for 'UniqBeforePluck cop' \
5-
do |method, source, action, corrected = nil|
6-
if action == :correct
7-
it "finds the use of #{method} after pluck in #{source}" do
8-
offenses = inspect_source(source)
9-
expect(offenses.first.message).to eq('Use `distinct` before `pluck`.')
10-
corrected_source = corrected || 'Model.distinct.pluck(:id)'
11-
expect(autocorrect_source(source)).to eq(corrected_source)
12-
end
13-
else
14-
it "ignores pluck without errors in #{source}" do
15-
expect_no_offenses(source)
16-
end
17-
end
4+
shared_examples_for 'mode-independent behavior' do
5+
it 'corrects' do
6+
expect_offense(<<~RUBY)
7+
Model.pluck(:name).uniq
8+
^^^^ Use `distinct` before `pluck`.
9+
RUBY
10+
11+
expect_correction(<<~RUBY)
12+
Model.distinct.pluck(:name)
13+
RUBY
1814
end
1915

20-
shared_examples_for 'mode independent behavior' do |method|
21-
it_behaves_like 'UniqBeforePluck cop', method,
22-
"Model.pluck(:id).#{method}", :correct
16+
it 'corrects hanging period' do
17+
expect_offense(<<~RUBY)
18+
Model.pluck(:name)
19+
.uniq
20+
^^^^ Use `distinct` before `pluck`.
21+
RUBY
2322

24-
it_behaves_like 'UniqBeforePluck cop', method,
25-
['Model.pluck(:id)',
26-
" .#{method}"].join("\n"), :correct
23+
expect_correction(<<~RUBY)
24+
Model.distinct.pluck(:name)
25+
RUBY
26+
end
2727

28-
it_behaves_like 'UniqBeforePluck cop', method,
29-
['Model.pluck(:id).',
30-
" #{method}"].join("\n"), :correct
28+
it 'corrects trailing period' do
29+
expect_offense(<<~RUBY)
30+
Model.pluck(:name).
31+
uniq
32+
^^^^ Use `distinct` before `pluck`.
33+
RUBY
3134

32-
context "#{method} before pluck" do
33-
it_behaves_like 'UniqBeforePluck cop', method,
34-
"Model.where(foo: 1).#{method}.pluck(:something)", :ignore
35+
expect_correction(<<~RUBY)
36+
Model.distinct.pluck(:name)
37+
RUBY
3538
end
3639

37-
context "#{method} without a receiver" do
38-
it_behaves_like 'UniqBeforePluck cop', method,
39-
"#{method}.something", :ignore
40+
it 'ignores uniq before pluck' do
41+
expect_no_offenses(<<~RUBY)
42+
Model.where(foo: 1).uniq.pluck(:something)
43+
RUBY
4044
end
4145

42-
context "#{method} without pluck" do
43-
it_behaves_like 'UniqBeforePluck cop', method,
44-
"Model.#{method}", :ignore
46+
it 'ignores uniq without a receiver' do
47+
expect_no_offenses(<<~RUBY)
48+
uniq.something
49+
RUBY
4550
end
4651

47-
context "#{method} with a block" do
48-
it_behaves_like 'UniqBeforePluck cop', method,
49-
"Model.where(foo: 1).pluck(:id).#{method} { |k| k[0] }",
50-
:ignore
52+
it 'ignores uniq without pluck' do
53+
expect_no_offenses(<<~RUBY)
54+
Model.uniq
55+
RUBY
5156
end
52-
end
53-
54-
shared_examples_for 'mode dependent offenses' do |method, action|
55-
it_behaves_like 'UniqBeforePluck cop', method,
56-
"Model.scope.pluck(:id).#{method}", action,
57-
'Model.scope.distinct.pluck(:id)'
5857

59-
it_behaves_like 'UniqBeforePluck cop', method,
60-
"instance.assoc.pluck(:id).#{method}", action,
61-
'instance.assoc.distinct.pluck(:id)'
58+
it 'ignores uniq with a block' do
59+
expect_no_offenses(<<~RUBY)
60+
Model.where(foo: 1).pluck(:name).uniq { |k| k[0] }
61+
RUBY
62+
end
6263
end
6364

6465
it 'registers an offense' do
6566
expect_offense(<<~RUBY)
66-
Model.pluck(:id).uniq
67-
^^^^ Use `distinct` before `pluck`.
67+
Model.pluck(:name).uniq
68+
^^^^ Use `distinct` before `pluck`.
6869
RUBY
6970
end
7071

71-
%w[uniq distinct].each do |method|
72-
context 'when the enforced mode is conservative' do
73-
let(:cop_config) do
74-
{ 'EnforcedStyle' => 'conservative', 'AutoCorrect' => true }
75-
end
72+
context 'when the enforced mode is conservative' do
73+
let(:cop_config) { { 'EnforcedStyle' => 'conservative' } }
7674

77-
it_behaves_like 'mode independent behavior', method
75+
it_behaves_like 'mode-independent behavior'
7876

79-
it_behaves_like 'mode dependent offenses', method, :ignore
77+
it 'ignores model with a scope' do
78+
expect_no_offenses(<<~RUBY)
79+
Model.scope.pluck(:name).uniq
80+
RUBY
8081
end
8182

82-
context 'when the enforced mode is aggressive' do
83-
let(:cop_config) do
84-
{ 'EnforcedStyle' => 'aggressive', 'AutoCorrect' => true }
85-
end
83+
it 'ignores uniq on an association' do
84+
expect_no_offenses(<<~RUBY)
85+
instance.assoc.pluck(:name).uniq
86+
RUBY
87+
end
88+
end
89+
90+
context 'when the enforced mode is aggressive' do
91+
let(:cop_config) { { 'EnforcedStyle' => 'aggressive' } }
92+
93+
it_behaves_like 'mode-independent behavior'
94+
95+
it 'corrects model with a scope' do
96+
expect_offense(<<~RUBY)
97+
Model.scope.pluck(:name).uniq
98+
^^^^ Use `distinct` before `pluck`.
99+
RUBY
100+
101+
expect_correction(<<~RUBY)
102+
Model.scope.distinct.pluck(:name)
103+
RUBY
104+
end
86105

87-
it_behaves_like 'mode independent behavior', method
106+
it 'corrects uniq on an association' do
107+
expect_offense(<<~RUBY)
108+
instance.assoc.pluck(:name).uniq
109+
^^^^ Use `distinct` before `pluck`.
110+
RUBY
88111

89-
it_behaves_like 'mode dependent offenses', method, :correct
112+
expect_correction(<<~RUBY)
113+
instance.assoc.distinct.pluck(:name)
114+
RUBY
90115
end
91116
end
92117
end

0 commit comments

Comments
 (0)