Skip to content

Commit 24560e7

Browse files
authored
Merge pull request #607 from natematykiewicz/duplicate_scope_cop
[Fix #427] Add Rails/DuplicateScope cop
2 parents 94a3c21 + 4177112 commit 24560e7

File tree

8 files changed

+104
-24
lines changed

8 files changed

+104
-24
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#427](https://github.com/rubocop/rubocop-rails/issues/427): Add `Rails/DuplicateScope` cop. ([@natematykiewicz][])

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ Rails/DuplicateAssociation:
266266
Enabled: pending
267267
VersionAdded: '<<next>>'
268268

269+
Rails/DuplicateScope:
270+
Description: 'Multiple scopes share this same where clause.'
271+
Enabled: pending
272+
VersionAdded: '<<next>>'
273+
269274
Rails/DurationArithmetic:
270275
Description: 'Do not use duration as arithmetic operand with `Time.current`.'
271276
StyleGuide: 'https://rails.rubystyle.guide#duration-arithmetic'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
# A mixin to return all of the class send nodes.
6+
module ClassSendNodeHelper
7+
def class_send_nodes(class_node)
8+
class_def = class_node.body
9+
10+
return [] unless class_def
11+
12+
if class_def.send_type?
13+
[class_def]
14+
else
15+
class_def.each_child_node(:send)
16+
end
17+
end
18+
end
19+
end
20+
end

lib/rubocop/cop/rails/after_commit_override.rb

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ module Rails
3232
# after_update_commit :log_update_action
3333
#
3434
class AfterCommitOverride < Base
35+
include ClassSendNodeHelper
36+
3537
MSG = 'There can only be one `after_*_commit :%<name>s` hook defined for a model.'
3638

3739
AFTER_COMMIT_CALLBACKS = %i[
@@ -63,18 +65,6 @@ def each_after_commit_callback(class_node)
6365
end
6466
end
6567

66-
def class_send_nodes(class_node)
67-
class_def = class_node.body
68-
69-
return [] unless class_def
70-
71-
if class_def.send_type?
72-
[class_def]
73-
else
74-
class_def.each_child_node(:send).to_a
75-
end
76-
end
77-
7868
def after_commit_callback?(node)
7969
AFTER_COMMIT_CALLBACKS.include?(node.method_name)
8070
end

lib/rubocop/cop/rails/duplicate_association.rb

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Rails
2323
class DuplicateAssociation < Base
2424
include RangeHelp
2525
extend AutoCorrector
26+
include ClassSendNodeHelper
2627

2728
MSG = "Association `%<name>s` is defined multiple times. Don't repeat associations."
2829

@@ -49,18 +50,6 @@ def offenses(class_node)
4950
.group_by { |node| association(node).to_sym }
5051
.select { |_, nodes| nodes.length > 1 }
5152
end
52-
53-
def class_send_nodes(class_node)
54-
class_def = class_node.body
55-
56-
return [] unless class_def
57-
58-
if class_def.send_type?
59-
[class_def]
60-
else
61-
class_def.each_child_node(:send)
62-
end
63-
end
6453
end
6554
end
6655
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Rails
6+
# This cop checks for multiple scopes in a model that have the same `where` clause. This
7+
# often means you copy/pasted a scope, updated the name, and forgot to change the condition.
8+
#
9+
# @example
10+
#
11+
# # bad
12+
# scope :visible, -> { where(visible: true) }
13+
# scope :hidden, -> { where(visible: true) }
14+
#
15+
# # good
16+
# scope :visible, -> { where(visible: true) }
17+
# scope :hidden, -> { where(visible: false) }
18+
#
19+
class DuplicateScope < Base
20+
include ClassSendNodeHelper
21+
22+
MSG = 'Multiple scopes share this same where clause.'
23+
24+
def_node_matcher :scope, <<~PATTERN
25+
(send nil? :scope _ $...)
26+
PATTERN
27+
28+
def on_class(class_node)
29+
offenses(class_node).each do |node|
30+
add_offense(node)
31+
end
32+
end
33+
34+
private
35+
36+
def offenses(class_node)
37+
class_send_nodes(class_node).select { |node| scope(node) }
38+
.group_by { |node| scope(node) }
39+
.select { |_, nodes| nodes.length > 1 }
40+
.values
41+
.flatten
42+
end
43+
end
44+
end
45+
end
46+
end

lib/rubocop/cop/rails_cops.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative 'mixin/active_record_helper'
44
require_relative 'mixin/active_record_migrations_helper'
5+
require_relative 'mixin/class_send_node_helper'
56
require_relative 'mixin/enforce_superclass'
67
require_relative 'mixin/index_method'
78
require_relative 'mixin/migrations_helper'
@@ -33,6 +34,7 @@
3334
require_relative 'rails/delegate'
3435
require_relative 'rails/delegate_allow_blank'
3536
require_relative 'rails/duplicate_association'
37+
require_relative 'rails/duplicate_scope'
3638
require_relative 'rails/duration_arithmetic'
3739
require_relative 'rails/dynamic_find_by'
3840
require_relative 'rails/eager_evaluation_log_message'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Rails::DuplicateScope, :config do
4+
it 'registers an offense when a duplicate scope is detected' do
5+
expect_offense(<<~RUBY)
6+
class Post < ApplicationRecord
7+
scope :visible, -> { where(visible: true) }
8+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Multiple scopes share this same where clause.
9+
scope :hidden, -> { where(visible: true) }
10+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Multiple scopes share this same where clause.
11+
scope :new, -> { where(created_at: 1.week.ago..Date.current) }
12+
scope :popular, -> { where(comments_count: 1000..Float::INFINITY) }
13+
end
14+
RUBY
15+
end
16+
17+
it 'does not register an offense when there are no duplicates' do
18+
expect_no_offenses(<<~RUBY)
19+
class Post < ApplicationRecord
20+
scope :visible, -> { where(visible: true) }
21+
scope :hidden, -> { where(visible: false) }
22+
scope :new, -> { where(created_at: 1.week.ago..Date.current) }
23+
scope :popular, -> { where(comments_count: 1000..Float::INFINITY) }
24+
end
25+
RUBY
26+
end
27+
end

0 commit comments

Comments
 (0)