rubocop-rails 2.13.0 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/config/default.yml +75 -4
  4. data/lib/rubocop/cop/mixin/class_send_node_helper.rb +20 -0
  5. data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
  6. data/lib/rubocop/cop/rails/action_controller_test_case.rb +47 -0
  7. data/lib/rubocop/cop/rails/after_commit_override.rb +2 -12
  8. data/lib/rubocop/cop/rails/bulk_change_table.rb +20 -6
  9. data/lib/rubocop/cop/rails/compact_blank.rb +22 -13
  10. data/lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb +108 -0
  11. data/lib/rubocop/cop/rails/duplicate_association.rb +56 -0
  12. data/lib/rubocop/cop/rails/duplicate_scope.rb +46 -0
  13. data/lib/rubocop/cop/rails/duration_arithmetic.rb +2 -1
  14. data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +94 -0
  15. data/lib/rubocop/cop/rails/i18n_locale_texts.rb +110 -0
  16. data/lib/rubocop/cop/rails/index_by.rb +6 -6
  17. data/lib/rubocop/cop/rails/index_with.rb +6 -6
  18. data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
  19. data/lib/rubocop/cop/rails/migration_class_name.rb +61 -0
  20. data/lib/rubocop/cop/rails/pluck.rb +15 -7
  21. data/lib/rubocop/cop/rails/read_write_attribute.rb +51 -14
  22. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +93 -28
  23. data/lib/rubocop/cop/rails/reversible_migration.rb +5 -3
  24. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +2 -10
  25. data/lib/rubocop/cop/rails/table_name_assignment.rb +44 -0
  26. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +77 -0
  27. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +2 -0
  28. data/lib/rubocop/cop/rails_cops.rb +11 -0
  29. data/lib/rubocop/rails/version.rb +1 -1
  30. metadata +15 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f12ec262c68bd72f514b80f2140b51278a947f21726ef6307e6267eaec7df4ae
4
- data.tar.gz: 2a09b4db338465d1904a19e8d4af871ba485e75244613299fabe473854125e4c
3
+ metadata.gz: bf7825df8f38360ce42c29846bd7b2e276447e6d87a5585acc35da05b829992f
4
+ data.tar.gz: 6f219da0c0e0b92f9b5027bd1e50381435a4ac660484e5164a2718e79d52e6c7
5
5
  SHA512:
6
- metadata.gz: fbf73dc2e978bc210e2dc190b8042720f24e6e374962b2873e6c29ec989ed2a7c1b05646c22a38d57f28e753104ac40a0e592730e63703d99dbb80fc6b498651
7
- data.tar.gz: b70e78355a3fa5b09f3a2a1603c71b16547c3f2f80c0ce7c2dbbc3c38cae0a0ca9464a50895eb54ac8ccd558c9259bf28a1e4ebebb9e3d4f05102832f178fc11
6
+ metadata.gz: e3ebdfbd8f0d547d3c5becc45b95f2faa4c36651397e2082ec5c454c0388cfed7b09edc8d3bfbbdd1d70ea09b8a171e08efedf6f0eb660e39b24df2888d92547
7
+ data.tar.gz: '06831b53608a82160e586d295aa04fe14670791545602386440fd9b40792391f2ddb8ac7d49b2dd677722251d7618c0f11388866bfd5dc1975b02ce99fb9246e'
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-21 Bozhidar Batsov
1
+ Copyright (c) 2012-22 Bozhidar Batsov
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/config/default.yml CHANGED
@@ -7,7 +7,9 @@ inherit_mode:
7
7
  AllCops:
8
8
  Exclude:
9
9
  - bin/*
10
- - db/schema.rb
10
+ # Exclude db/schema.rb and db/[CONFIGURATION_NAMESPACE]_schema.rb by default.
11
+ # See: https://guides.rubyonrails.org/active_record_multiple_databases.html#setting-up-your-application
12
+ - db/*schema.rb
11
13
  # What version of Rails is the inspected code using? If a value is specified
12
14
  # for TargetRailsVersion then it is used. Acceptable values are specified
13
15
  # as a float (i.e. 5.1); the patch version of Rails should not be included.
@@ -38,6 +40,16 @@ Lint/NumberConversion:
38
40
  - fortnights
39
41
  - in_milliseconds
40
42
 
43
+ Rails/ActionControllerTestCase:
44
+ Description: 'Use `ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`.'
45
+ StyleGuide: 'https://rails.rubystyle.guide/#integration-testing'
46
+ Reference: 'https://api.rubyonrails.org/classes/ActionController/TestCase.html'
47
+ Enabled: 'pending'
48
+ SafeAutocorrect: false
49
+ VersionAdded: '2.14'
50
+ Include:
51
+ - '**/test/**/*.rb'
52
+
41
53
  Rails/ActionFilter:
42
54
  Description: 'Enforces consistent use of action filter methods.'
43
55
  Enabled: true
@@ -195,9 +207,12 @@ Rails/ContentTag:
195
207
  Enabled: true
196
208
  VersionAdded: '2.6'
197
209
  VersionChanged: '2.12'
198
- # This `Exclude` config prevents false positives for `tag` calls to `has_one: tag`. No helpers are used in normal models.
210
+ # This `Exclude` config prevents false positives for `tag` calls to `has_one: tag` and Puma configuration:
211
+ # https://puma.io/puma/Puma/DSL.html#tag-instance_method
212
+ # No helpers are used in normal models and configs.
199
213
  Exclude:
200
214
  - app/models/**/*.rb
215
+ - config/**/*.rb
201
216
 
202
217
  Rails/CreateTableWithTimestamps:
203
218
  Description: >-
@@ -251,6 +266,22 @@ Rails/DelegateAllowBlank:
251
266
  Enabled: true
252
267
  VersionAdded: '0.44'
253
268
 
269
+ Rails/DeprecatedActiveModelErrorsMethods:
270
+ Description: 'Avoid manipulating ActiveModel errors hash directly.'
271
+ Enabled: pending
272
+ Safe: false
273
+ VersionAdded: '2.14'
274
+
275
+ Rails/DuplicateAssociation:
276
+ Description: "Don't repeat associations in a model."
277
+ Enabled: pending
278
+ VersionAdded: '2.14'
279
+
280
+ Rails/DuplicateScope:
281
+ Description: 'Multiple scopes share this same where clause.'
282
+ Enabled: pending
283
+ VersionAdded: '2.14'
284
+
254
285
  Rails/DurationArithmetic:
255
286
  Description: 'Do not use duration as arithmetic operand with `Time.current`.'
256
287
  StyleGuide: 'https://rails.rubystyle.guide#duration-arithmetic'
@@ -415,6 +446,15 @@ Rails/HttpStatus:
415
446
  - numeric
416
447
  - symbolic
417
448
 
449
+ Rails/I18nLazyLookup:
450
+ Description: 'Checks for places where I18n "lazy" lookup can be used.'
451
+ StyleGuide: 'https://rails.rubystyle.guide/#lazy-lookup'
452
+ Reference: 'https://guides.rubyonrails.org/i18n.html#lazy-lookup'
453
+ Enabled: pending
454
+ VersionAdded: '2.14'
455
+ Include:
456
+ - 'controllers/**/*'
457
+
418
458
  Rails/I18nLocaleAssignment:
419
459
  Description: 'Prefer the usage of `I18n.with_locale` instead of manually updating `I18n.locale` value.'
420
460
  Enabled: 'pending'
@@ -423,6 +463,12 @@ Rails/I18nLocaleAssignment:
423
463
  - spec/**/*.rb
424
464
  - test/**/*.rb
425
465
 
466
+ Rails/I18nLocaleTexts:
467
+ Description: 'Enforces use of I18n and locale files instead of locale specific strings.'
468
+ StyleGuide: 'https://rails.rubystyle.guide/#locale-texts'
469
+ Enabled: pending
470
+ VersionAdded: '2.14'
471
+
426
472
  Rails/IgnoredSkipActionFilterOption:
427
473
  Description: 'Checks that `if` and `only` (or `except`) are not used together as options of `skip_*` action filter.'
428
474
  Reference: 'https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-_normalize_callback_options'
@@ -453,6 +499,7 @@ Rails/InverseOf:
453
499
  Description: 'Checks for associations where the inverse cannot be determined automatically.'
454
500
  Enabled: true
455
501
  VersionAdded: '0.52'
502
+ IgnoreScopes: false
456
503
  Include:
457
504
  - app/models/**/*.rb
458
505
 
@@ -494,6 +541,13 @@ Rails/MatchRoute:
494
541
  - config/routes.rb
495
542
  - config/routes/**/*.rb
496
543
 
544
+ Rails/MigrationClassName:
545
+ Description: 'The class name of the migration should match its file name.'
546
+ Enabled: pending
547
+ VersionAdded: '2.14'
548
+ Include:
549
+ - db/migrate/*.rb
550
+
497
551
  Rails/NegateInclude:
498
552
  Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
499
553
  StyleGuide: 'https://rails.rubystyle.guide#exclude'
@@ -627,6 +681,7 @@ Rails/RedundantForeignKey:
627
681
  Rails/RedundantPresenceValidationOnBelongsTo:
628
682
  Description: 'Checks for redundant presence validation on belongs_to association.'
629
683
  Enabled: pending
684
+ SafeAutoCorrect: false
630
685
  VersionAdded: '2.13'
631
686
 
632
687
  Rails/RedundantReceiverInWithOptions:
@@ -702,15 +757,17 @@ Rails/ReversibleMigration:
702
757
  Reference: 'https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html'
703
758
  Enabled: true
704
759
  VersionAdded: '0.47'
760
+ VersionChanged: '2.13'
705
761
  Include:
706
- - db/migrate/*.rb
762
+ - db/**/*.rb
707
763
 
708
764
  Rails/ReversibleMigrationMethodDefinition:
709
765
  Description: 'Checks whether the migration implements either a `change` method or both an `up` and a `down` method.'
710
766
  Enabled: false
711
767
  VersionAdded: '2.10'
768
+ VersionChanged: '2.13'
712
769
  Include:
713
- - db/migrate/*.rb
770
+ - db/**/*.rb
714
771
 
715
772
  Rails/RootJoinChain:
716
773
  Description: 'Use a single `#join` instead of chaining on `Rails.root` or `Rails.public_path`.'
@@ -813,6 +870,15 @@ Rails/SquishedSQLHeredocs:
813
870
  # to be preserved in order to work, thus auto-correction is not safe.
814
871
  SafeAutoCorrect: false
815
872
 
873
+ Rails/TableNameAssignment:
874
+ Description: >-
875
+ Do not use `self.table_name =`. Use Inflections or `table_name_prefix` instead.
876
+ StyleGuide: 'https://rails.rubystyle.guide/#keep-ar-defaults'
877
+ Enabled: false
878
+ VersionAdded: '2.14'
879
+ Include:
880
+ - app/models/**/*.rb
881
+
816
882
  Rails/TimeZone:
817
883
  Description: 'Checks the correct usage of time zone aware methods.'
818
884
  StyleGuide: 'https://rails.rubystyle.guide#time'
@@ -839,6 +905,11 @@ Rails/TimeZoneAssignment:
839
905
  - spec/**/*.rb
840
906
  - test/**/*.rb
841
907
 
908
+ Rails/TransactionExitStatement:
909
+ Description: 'Avoid the usage of `return`, `break` and `throw` in transaction blocks.'
910
+ Enabled: pending
911
+ VersionAdded: '2.14'
912
+
842
913
  Rails/UniqBeforePluck:
843
914
  Description: 'Prefer the use of uniq or distinct before pluck.'
844
915
  Enabled: true
@@ -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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for cops working with migrations
6
+ module MigrationsHelper
7
+ extend NodePattern::Macros
8
+
9
+ def_node_matcher :migration_class?, <<~PATTERN
10
+ (class
11
+ (const nil? _)
12
+ (send
13
+ (const (const {nil? cbase} :ActiveRecord) :Migration)
14
+ :[]
15
+ (float _))
16
+ _)
17
+ PATTERN
18
+
19
+ def in_migration?(node)
20
+ node.each_ancestor(:class).any? do |class_node|
21
+ migration_class?(class_node)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Using `ActionController::TestCase`` is discouraged and should be replaced by
7
+ # `ActionDispatch::IntegrationTest``. Controller tests are too close to the
8
+ # internals of a controller whereas integration tests mimic the browser/user.
9
+ #
10
+ # @safety
11
+ # This cop's autocorrection is unsafe because the API of each test case class is different.
12
+ # Make sure to update each test of your controller test cases after changing the superclass.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class MyControllerTest < ActionController::TestCase
17
+ # end
18
+ #
19
+ # # good
20
+ # class MyControllerTest < ActionDispatch::IntegrationTest
21
+ # end
22
+ #
23
+ class ActionControllerTestCase < Base
24
+ extend AutoCorrector
25
+ extend TargetRailsVersion
26
+
27
+ MSG = 'Use `ActionDispatch::IntegrationTest` instead.'
28
+
29
+ minimum_target_rails_version 5.0
30
+
31
+ def_node_matcher :action_controller_test_case?, <<~PATTERN
32
+ (class
33
+ (const nil? _)
34
+ (const (const {nil? cbase} :ActionController) :TestCase) nil?)
35
+ PATTERN
36
+
37
+ def on_class(node)
38
+ return unless action_controller_test_case?(node)
39
+
40
+ add_offense(node.parent_class) do |corrector|
41
+ corrector.replace(node.parent_class, 'ActionDispatch::IntegrationTest')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -32,6 +32,8 @@ module RuboCop
32
32
  # after_update_commit :log_update_action
33
33
  #
34
34
  class AfterCommitOverride < Base
35
+ include ClassSendNodeHelper
36
+
35
37
  MSG = 'There can only be one `after_*_commit :%<name>s` hook defined for a model.'
36
38
 
37
39
  AFTER_COMMIT_CALLBACKS = %i[
@@ -63,18 +65,6 @@ module RuboCop
63
65
  end
64
66
  end
65
67
 
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
-
78
68
  def after_commit_callback?(node)
79
69
  AFTER_COMMIT_CALLBACKS.include?(node.method_name)
80
70
  end
@@ -155,17 +155,31 @@ module RuboCop
155
155
  return if include_bulk_options?(node)
156
156
  return unless node.block_node
157
157
 
158
- send_nodes = node.block_node.body.each_child_node(:send).to_a
158
+ send_nodes = send_nodes_from_change_table_block(node.block_node.body)
159
159
 
160
- transformations = send_nodes.select do |send_node|
161
- combinable_transformations.include?(send_node.method_name)
162
- end
163
-
164
- add_offense_for_change_table(node) if transformations.size > 1
160
+ add_offense_for_change_table(node) if count_transformations(send_nodes) > 1
165
161
  end
166
162
 
167
163
  private
168
164
 
165
+ def send_nodes_from_change_table_block(body)
166
+ if body.send_type?
167
+ [body]
168
+ else
169
+ body.each_child_node(:send).to_a
170
+ end
171
+ end
172
+
173
+ def count_transformations(send_nodes)
174
+ send_nodes.sum do |node|
175
+ if node.method?(:remove)
176
+ node.arguments.count { |arg| !arg.hash_type? }
177
+ else
178
+ combinable_transformations.include?(node.method_name) ? 1 : 0
179
+ end
180
+ end
181
+ end
182
+
169
183
  # @param node [RuboCop::AST::SendNode] (send nil? :change_table ...)
170
184
  def include_bulk_options?(node)
171
185
  # arguments: [{(sym :table)(str "table")} (hash (pair (sym :bulk) _))]
@@ -10,25 +10,29 @@ module RuboCop
10
10
  # blank check of block arguments to the receiver object.
11
11
  #
12
12
  # For example, `[[1, 2], [3, nil]].reject { |first, second| second.blank? }` and
13
- # `[[1, 2], [3, nil]].compact_blank` are not compatible. The same is true for `empty?`.
13
+ # `[[1, 2], [3, nil]].compact_blank` are not compatible. The same is true for `blank?`.
14
14
  # This will work fine when the receiver is a hash object.
15
15
  #
16
+ # And `compact_blank!` has different implementations for `Array`, `Hash`, and
17
+ # `ActionController::Parameters`.
18
+ # `Array#compact_blank!`, `Hash#compact_blank!` are equivalent to `delete_if(&:blank?)`.
19
+ # `ActionController::Parameters#compact_blank!` is equivalent to `reject!(&:blank?)`.
20
+ # If the cop makes a mistake, auto-corrected code may get unexpected behavior.
21
+ #
16
22
  # @example
17
23
  #
18
24
  # # bad
19
25
  # collection.reject(&:blank?)
20
- # collection.reject(&:empty?)
21
26
  # collection.reject { |_k, v| v.blank? }
22
- # collection.reject { |_k, v| v.empty? }
23
27
  #
24
28
  # # good
25
29
  # collection.compact_blank
26
30
  #
27
31
  # # bad
28
- # collection.reject!(&:blank?)
29
- # collection.reject!(&:empty?)
30
- # collection.reject! { |_k, v| v.blank? }
31
- # collection.reject! { |_k, v| v.empty? }
32
+ # collection.delete_if(&:blank?) # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
33
+ # collection.delete_if { |_k, v| v.blank? } # Same behavior as `Array#compact_blank!` and `Hash#compact_blank!`
34
+ # collection.reject!(&:blank?) # Same behavior as `ActionController::Parameters#compact_blank!`
35
+ # collection.reject! { |_k, v| v.blank? } # Same behavior as `ActionController::Parameters#compact_blank!`
32
36
  #
33
37
  # # good
34
38
  # collection.compact_blank!
@@ -39,22 +43,22 @@ module RuboCop
39
43
  extend TargetRailsVersion
40
44
 
41
45
  MSG = 'Use `%<preferred_method>s` instead.'
42
- RESTRICT_ON_SEND = %i[reject reject!].freeze
46
+ RESTRICT_ON_SEND = %i[reject delete_if reject!].freeze
43
47
 
44
48
  minimum_target_rails_version 6.1
45
49
 
46
50
  def_node_matcher :reject_with_block?, <<~PATTERN
47
51
  (block
48
- (send _ {:reject :reject!})
52
+ (send _ {:reject :delete_if :reject!})
49
53
  $(args ...)
50
54
  (send
51
- $(lvar _) {:blank? :empty?}))
55
+ $(lvar _) :blank?))
52
56
  PATTERN
53
57
 
54
58
  def_node_matcher :reject_with_block_pass?, <<~PATTERN
55
- (send _ {:reject :reject!}
59
+ (send _ {:reject :delete_if :reject!}
56
60
  (block_pass
57
- (sym {:blank? :empty?})))
61
+ (sym :blank?)))
58
62
  PATTERN
59
63
 
60
64
  def on_send(node)
@@ -73,12 +77,17 @@ module RuboCop
73
77
  return true if reject_with_block_pass?(node)
74
78
 
75
79
  if (arguments, receiver_in_block = reject_with_block?(node.parent))
76
- return arguments.length == 1 || use_hash_value_block_argument?(arguments, receiver_in_block)
80
+ return use_single_value_block_argument?(arguments, receiver_in_block) ||
81
+ use_hash_value_block_argument?(arguments, receiver_in_block)
77
82
  end
78
83
 
79
84
  false
80
85
  end
81
86
 
87
+ def use_single_value_block_argument?(arguments, receiver_in_block)
88
+ arguments.length == 1 && arguments[0].source == receiver_in_block.source
89
+ end
90
+
82
91
  def use_hash_value_block_argument?(arguments, receiver_in_block)
83
92
  arguments.length == 2 && arguments[1].source == receiver_in_block.source
84
93
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks direct manipulation of ActiveModel#errors as hash.
7
+ # These operations are deprecated in Rails 6.1 and will not work in Rails 7.
8
+ #
9
+ # @safety
10
+ # This cop is unsafe because it can report `errors` manipulation on non-ActiveModel,
11
+ # which is obviously valid.
12
+ # The cop has no way of knowing whether a variable is an ActiveModel or not.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # user.errors[:name] << 'msg'
17
+ # user.errors.messages[:name] << 'msg'
18
+ #
19
+ # # good
20
+ # user.errors.add(:name, 'msg')
21
+ #
22
+ # # bad
23
+ # user.errors[:name].clear
24
+ # user.errors.messages[:name].clear
25
+ #
26
+ # # good
27
+ # user.errors.delete(:name)
28
+ #
29
+ class DeprecatedActiveModelErrorsMethods < Base
30
+ MSG = 'Avoid manipulating ActiveModel errors as hash directly.'
31
+
32
+ MANIPULATIVE_METHODS = Set[
33
+ *%i[
34
+ << append clear collect! compact! concat
35
+ delete delete_at delete_if drop drop_while fill filter! keep_if
36
+ flatten! insert map! pop prepend push reject! replace reverse!
37
+ rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift
38
+ ]
39
+ ].freeze
40
+
41
+ def_node_matcher :receiver_matcher_outside_model, '{send ivar lvar}'
42
+ def_node_matcher :receiver_matcher_inside_model, '{nil? send ivar lvar}'
43
+
44
+ def_node_matcher :any_manipulation?, <<~PATTERN
45
+ {
46
+ #root_manipulation?
47
+ #root_assignment?
48
+ #messages_details_manipulation?
49
+ #messages_details_assignment?
50
+ }
51
+ PATTERN
52
+
53
+ def_node_matcher :root_manipulation?, <<~PATTERN
54
+ (send
55
+ (send
56
+ (send #receiver_matcher :errors) :[] ...)
57
+ MANIPULATIVE_METHODS
58
+ ...
59
+ )
60
+ PATTERN
61
+
62
+ def_node_matcher :root_assignment?, <<~PATTERN
63
+ (send
64
+ (send #receiver_matcher :errors)
65
+ :[]=
66
+ ...)
67
+ PATTERN
68
+
69
+ def_node_matcher :messages_details_manipulation?, <<~PATTERN
70
+ (send
71
+ (send
72
+ (send
73
+ (send #receiver_matcher :errors)
74
+ {:messages :details})
75
+ :[]
76
+ ...)
77
+ MANIPULATIVE_METHODS
78
+ ...)
79
+ PATTERN
80
+
81
+ def_node_matcher :messages_details_assignment?, <<~PATTERN
82
+ (send
83
+ (send
84
+ (send #receiver_matcher :errors)
85
+ {:messages :details})
86
+ :[]=
87
+ ...)
88
+ PATTERN
89
+
90
+ def on_send(node)
91
+ any_manipulation?(node) do
92
+ add_offense(node)
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def receiver_matcher(node)
99
+ model_file? ? receiver_matcher_inside_model(node) : receiver_matcher_outside_model(node)
100
+ end
101
+
102
+ def model_file?
103
+ processed_source.buffer.name.include?('/models/')
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for associations that have been defined multiple times in the same file.
7
+ #
8
+ # When an association is defined multiple times on a model, Active Record overrides the
9
+ # previously defined association with the new one. Because of this, this cop's autocorrection
10
+ # simply keeps the last of any duplicates and discards the rest.
11
+ #
12
+ # @example
13
+ #
14
+ # # bad
15
+ # belongs_to :foo
16
+ # belongs_to :bar
17
+ # has_one :foo
18
+ #
19
+ # # good
20
+ # belongs_to :bar
21
+ # has_one :foo
22
+ #
23
+ class DuplicateAssociation < Base
24
+ include RangeHelp
25
+ extend AutoCorrector
26
+ include ClassSendNodeHelper
27
+
28
+ MSG = "Association `%<name>s` is defined multiple times. Don't repeat associations."
29
+
30
+ def_node_matcher :association, <<~PATTERN
31
+ (send nil? {:belongs_to :has_one :has_many :has_and_belongs_to_many} ({sym str} $_) ...)
32
+ PATTERN
33
+
34
+ def on_class(class_node)
35
+ offenses(class_node).each do |name, nodes|
36
+ nodes.each do |node|
37
+ add_offense(node, message: format(MSG, name: name)) do |corrector|
38
+ next if nodes.last == node
39
+
40
+ corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def offenses(class_node)
49
+ class_send_nodes(class_node).select { |node| association(node) }
50
+ .group_by { |node| association(node).to_sym }
51
+ .select { |_, nodes| nodes.length > 1 }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -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
@@ -26,7 +26,8 @@ module RuboCop
26
26
  RESTRICT_ON_SEND = %i[+ -].freeze
27
27
 
28
28
  DURATIONS = Set[:second, :seconds, :minute, :minutes, :hour, :hours,
29
- :day, :days, :week, :weeks, :fortnight, :fortnights]
29
+ :day, :days, :week, :weeks, :fortnight, :fortnights,
30
+ :month, :months, :year, :years]
30
31
 
31
32
  # @!method duration_arithmetic_argument?(node)
32
33
  # Match duration subtraction or addition with current time.