rubocop-rails 2.13.2 → 2.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87ab88a596e8f8dbe2ef0ded9a93ae1ceba10572ad41fc6b4e1b0b684cf68110
4
- data.tar.gz: 4bc8502424aa32efec36f06ca475b7a3918b160060f76aa65eba39bfa5a31947
3
+ metadata.gz: bf7825df8f38360ce42c29846bd7b2e276447e6d87a5585acc35da05b829992f
4
+ data.tar.gz: 6f219da0c0e0b92f9b5027bd1e50381435a4ac660484e5164a2718e79d52e6c7
5
5
  SHA512:
6
- metadata.gz: 27016cd823ac6eb75ddb06f97c613fea178b29dc8cadac6289c803a9745ccbe2ffd7f7799c82b1a9ddd74f08ab162fcb50acf9a491a5841f1d09931eec8145fa
7
- data.tar.gz: 95c826339433a0a87f296676216e62b493cdc4d5b67ac73c400659194d3a54e874554d0431c3cbea02316d4265cd870755320ed5f5fd6b42722fdd17feba041e
6
+ metadata.gz: e3ebdfbd8f0d547d3c5becc45b95f2faa4c36651397e2082ec5c454c0388cfed7b09edc8d3bfbbdd1d70ea09b8a171e08efedf6f0eb660e39b24df2888d92547
7
+ data.tar.gz: '06831b53608a82160e586d295aa04fe14670791545602386440fd9b40792391f2ddb8ac7d49b2dd677722251d7618c0f11388866bfd5dc1975b02ce99fb9246e'
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'
@@ -495,6 +541,13 @@ Rails/MatchRoute:
495
541
  - config/routes.rb
496
542
  - config/routes/**/*.rb
497
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
+
498
551
  Rails/NegateInclude:
499
552
  Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
500
553
  StyleGuide: 'https://rails.rubystyle.guide#exclude'
@@ -817,6 +870,15 @@ Rails/SquishedSQLHeredocs:
817
870
  # to be preserved in order to work, thus auto-correction is not safe.
818
871
  SafeAutoCorrect: false
819
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
+
820
882
  Rails/TimeZone:
821
883
  Description: 'Checks the correct usage of time zone aware methods.'
822
884
  StyleGuide: 'https://rails.rubystyle.guide#time'
@@ -843,6 +905,11 @@ Rails/TimeZoneAssignment:
843
905
  - spec/**/*.rb
844
906
  - test/**/*.rb
845
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
+
846
913
  Rails/UniqBeforePluck:
847
914
  Description: 'Prefer the use of uniq or distinct before pluck.'
848
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,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) _))]
@@ -13,6 +13,12 @@ module RuboCop
13
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
@@ -23,8 +29,10 @@ module RuboCop
23
29
  # collection.compact_blank
24
30
  #
25
31
  # # bad
26
- # collection.reject!(&:blank?)
27
- # collection.reject! { |_k, v| v.blank? }
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!`
28
36
  #
29
37
  # # good
30
38
  # collection.compact_blank!
@@ -35,20 +43,20 @@ module RuboCop
35
43
  extend TargetRailsVersion
36
44
 
37
45
  MSG = 'Use `%<preferred_method>s` instead.'
38
- RESTRICT_ON_SEND = %i[reject reject!].freeze
46
+ RESTRICT_ON_SEND = %i[reject delete_if reject!].freeze
39
47
 
40
48
  minimum_target_rails_version 6.1
41
49
 
42
50
  def_node_matcher :reject_with_block?, <<~PATTERN
43
51
  (block
44
- (send _ {:reject :reject!})
52
+ (send _ {:reject :delete_if :reject!})
45
53
  $(args ...)
46
54
  (send
47
55
  $(lvar _) :blank?))
48
56
  PATTERN
49
57
 
50
58
  def_node_matcher :reject_with_block_pass?, <<~PATTERN
51
- (send _ {:reject :reject!}
59
+ (send _ {:reject :delete_if :reject!}
52
60
  (block_pass
53
61
  (sym :blank?)))
54
62
  PATTERN
@@ -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
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for places where I18n "lazy" lookup can be used.
7
+ #
8
+ # @example
9
+ # # en.yml
10
+ # # en:
11
+ # # books:
12
+ # # create:
13
+ # # success: Book created!
14
+ #
15
+ # # bad
16
+ # class BooksController < ApplicationController
17
+ # def create
18
+ # # ...
19
+ # redirect_to books_url, notice: t('books.create.success')
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ # class BooksController < ApplicationController
25
+ # def create
26
+ # # ...
27
+ # redirect_to books_url, notice: t('.success')
28
+ # end
29
+ # end
30
+ #
31
+ class I18nLazyLookup < Base
32
+ include VisibilityHelp
33
+ extend AutoCorrector
34
+
35
+ MSG = 'Use "lazy" lookup for the text used in controllers.'
36
+
37
+ def_node_matcher :translate_call?, <<~PATTERN
38
+ (send nil? {:translate :t} ${sym_type? str_type?} ...)
39
+ PATTERN
40
+
41
+ def on_send(node)
42
+ translate_call?(node) do |key_node|
43
+ key = key_node.value
44
+ return if key.to_s.start_with?('.')
45
+
46
+ controller, action = controller_and_action(node)
47
+ return unless controller && action
48
+
49
+ scoped_key = get_scoped_key(key_node, controller, action)
50
+ return unless key == scoped_key
51
+
52
+ add_offense(key_node) do |corrector|
53
+ unscoped_key = key_node.value.to_s.split('.').last
54
+ corrector.replace(key_node, "'.#{unscoped_key}'")
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def controller_and_action(node)
62
+ action_node = node.each_ancestor(:def).first
63
+ return unless action_node && node_visibility(action_node) == :public
64
+
65
+ controller_node = node.each_ancestor(:class).first
66
+ return unless controller_node && controller_node.identifier.source.end_with?('Controller')
67
+
68
+ [controller_node, action_node]
69
+ end
70
+
71
+ def get_scoped_key(key_node, controller, action)
72
+ path = controller_path(controller).tr('/', '.')
73
+ action_name = action.method_name
74
+ key = key_node.value.to_s.split('.').last
75
+
76
+ "#{path}.#{action_name}.#{key}"
77
+ end
78
+
79
+ def controller_path(controller)
80
+ module_name = controller.parent_module_name
81
+ controller_name = controller.identifier.source
82
+
83
+ path = if module_name == 'Object'
84
+ controller_name
85
+ else
86
+ "#{module_name}::#{controller_name}"
87
+ end
88
+
89
+ path.delete_suffix('Controller').underscore
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Enforces use of I18n and locale files instead of locale specific strings.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class User < ApplicationRecord
11
+ # validates :email, presence: { message: "must be present" }
12
+ # end
13
+ #
14
+ # # good
15
+ # # config/locales/en.yml
16
+ # # en:
17
+ # # activerecord:
18
+ # # errors:
19
+ # # models:
20
+ # # user:
21
+ # # blank: "must be present"
22
+ #
23
+ # class User < ApplicationRecord
24
+ # validates :email, presence: true
25
+ # end
26
+ #
27
+ # # bad
28
+ # class PostsController < ApplicationController
29
+ # def create
30
+ # # ...
31
+ # redirect_to root_path, notice: "Post created!"
32
+ # end
33
+ # end
34
+ #
35
+ # # good
36
+ # # config/locales/en.yml
37
+ # # en:
38
+ # # posts:
39
+ # # create:
40
+ # # success: "Post created!"
41
+ #
42
+ # class PostsController < ApplicationController
43
+ # def create
44
+ # # ...
45
+ # redirect_to root_path, notice: t(".success")
46
+ # end
47
+ # end
48
+ #
49
+ # # bad
50
+ # class UserMailer < ApplicationMailer
51
+ # def welcome(user)
52
+ # mail(to: user.email, subject: "Welcome to My Awesome Site")
53
+ # end
54
+ # end
55
+ #
56
+ # # good
57
+ # # config/locales/en.yml
58
+ # # en:
59
+ # # user_mailer:
60
+ # # welcome:
61
+ # # subject: "Welcome to My Awesome Site"
62
+ #
63
+ # class UserMailer < ApplicationMailer
64
+ # def welcome(user)
65
+ # mail(to: user.email)
66
+ # end
67
+ # end
68
+ #
69
+ class I18nLocaleTexts < Base
70
+ MSG = 'Move locale texts to the locale files in the `config/locales` directory.'
71
+
72
+ RESTRICT_ON_SEND = %i[validates redirect_to []= mail].freeze
73
+
74
+ def_node_search :validation_message, <<~PATTERN
75
+ (pair (sym :message) $str)
76
+ PATTERN
77
+
78
+ def_node_search :redirect_to_flash, <<~PATTERN
79
+ (pair (sym {:notice :alert}) $str)
80
+ PATTERN
81
+
82
+ def_node_matcher :flash_assignment?, <<~PATTERN
83
+ (send (send nil? :flash) :[]= _ $str)
84
+ PATTERN
85
+
86
+ def_node_search :mail_subject, <<~PATTERN
87
+ (pair (sym :subject) $str)
88
+ PATTERN
89
+
90
+ def on_send(node)
91
+ case node.method_name
92
+ when :validates
93
+ validation_message(node) do |text_node|
94
+ add_offense(text_node)
95
+ end
96
+ return
97
+ when :redirect_to
98
+ text_node = redirect_to_flash(node).to_a.last
99
+ when :[]=
100
+ text_node = flash_assignment?(node)
101
+ when :mail
102
+ text_node = mail_subject(node).to_a.last
103
+ end
104
+
105
+ add_offense(text_node) if text_node
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop makes sure that each migration file defines a migration class
7
+ # whose name matches the file name.
8
+ # (e.g. `20220224111111_create_users.rb` should define `CreateUsers` class.)
9
+ #
10
+ # @example
11
+ # # db/migrate/20220224111111_create_users.rb
12
+ #
13
+ # # bad
14
+ # class SellBooks < ActiveRecord::Migration[7.0]
15
+ # end
16
+ #
17
+ # # good
18
+ # class CreateUsers < ActiveRecord::Migration[7.0]
19
+ # end
20
+ #
21
+ class MigrationClassName < Base
22
+ extend AutoCorrector
23
+
24
+ MSG = 'Replace with `%<corrected_class_name>s` that matches the file name.'
25
+
26
+ def on_class(node)
27
+ snake_class_name = to_snakecase(node.identifier.source)
28
+
29
+ return if snake_class_name == basename_without_timestamp
30
+
31
+ corrected_class_name = to_camelcase(basename_without_timestamp)
32
+ message = format(MSG, corrected_class_name: corrected_class_name)
33
+
34
+ add_offense(node.identifier, message: message) do |corrector|
35
+ corrector.replace(node.identifier, corrected_class_name)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def basename_without_timestamp
42
+ filepath = processed_source.file_path
43
+ basename = File.basename(filepath, '.rb')
44
+ basename.sub(/\A\d+_/, '')
45
+ end
46
+
47
+ def to_camelcase(word)
48
+ word.split('_').map(&:capitalize).join
49
+ end
50
+
51
+ def to_snakecase(word)
52
+ word
53
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
54
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
55
+ .tr('-', '_')
56
+ .downcase
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -21,25 +21,31 @@ module RuboCop
21
21
  extend AutoCorrector
22
22
  extend TargetRailsVersion
23
23
 
24
- MSG = 'Prefer `pluck(:%<value>s)` over `%<method>s { |%<argument>s| %<element>s[:%<value>s] }`.'
24
+ MSG = 'Prefer `pluck(:%<value>s)` over `%<current>s`.'
25
25
 
26
26
  minimum_target_rails_version 5.0
27
27
 
28
28
  def_node_matcher :pluck_candidate?, <<~PATTERN
29
- (block (send _ ${:map :collect}) (args (arg $_argument)) (send (lvar $_element) :[] (sym $_value)))
29
+ ({block numblock} (send _ {:map :collect}) $_argument (send (lvar $_element) :[] (sym $_value)))
30
30
  PATTERN
31
31
 
32
32
  def on_block(node)
33
- pluck_candidate?(node) do |method, argument, element, value|
34
- next unless argument == element
33
+ pluck_candidate?(node) do |argument, element, value|
34
+ match = if node.block_type?
35
+ argument.children.first.source.to_sym == element
36
+ else # numblock
37
+ argument == 1 && element == :_1
38
+ end
39
+ next unless match
35
40
 
36
- message = message(method, argument, element, value)
41
+ message = message(value, node)
37
42
 
38
43
  add_offense(offense_range(node), message: message) do |corrector|
39
44
  corrector.replace(offense_range(node), "pluck(:#{value})")
40
45
  end
41
46
  end
42
47
  end
48
+ alias on_numblock on_block
43
49
 
44
50
  private
45
51
 
@@ -47,8 +53,10 @@ module RuboCop
47
53
  node.send_node.loc.selector.join(node.loc.end)
48
54
  end
49
55
 
50
- def message(method, argument, element, value)
51
- format(MSG, method: method, argument: argument, element: element, value: value)
56
+ def message(value, node)
57
+ current = offense_range(node).source
58
+
59
+ format(MSG, value: value, current: current)
52
60
  end
53
61
  end
54
62
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces the absence of explicit table name assignment.
7
+ #
8
+ # `self.table_name=` should only be used for very good reasons,
9
+ # such as not having control over the database, or working
10
+ # on a legacy project.
11
+ #
12
+ # If you need to change how your model's name is translated to
13
+ # a table name, you may want to look at Inflections:
14
+ # https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html
15
+ #
16
+ # If you wish to add a prefix in front of your model, or wish to change
17
+ # the default prefix, `self.table_name_prefix` might better suit your needs:
18
+ # https://api.rubyonrails.org/classes/ActiveRecord/ModelSchema.html#method-c-table_name_prefix-3D
19
+ #
20
+ # STI base classes named `Base` are ignored by this cop.
21
+ # For more information: https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
22
+ #
23
+ # @example
24
+ # # bad
25
+ # self.table_name = 'some_table_name'
26
+ # self.table_name = :some_other_name
27
+ class TableNameAssignment < Base
28
+ include ActiveRecordHelper
29
+
30
+ MSG = 'Do not use `self.table_name =`.'
31
+
32
+ def_node_matcher :base_class?, <<~PATTERN
33
+ (class (const ... :Base) ...)
34
+ PATTERN
35
+
36
+ def on_class(class_node)
37
+ return if base_class?(class_node)
38
+
39
+ find_set_table_name(class_node).each { |node| add_offense(node) }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for the use of exit statements (namely `return`,
7
+ # `break` and `throw`) in transactions. This is due to the eventual
8
+ # unexpected behavior when using ActiveRecord >= 7, where transactions
9
+ # exitted using these statements are being rollbacked rather than
10
+ # committed (pre ActiveRecord 7 behavior).
11
+ #
12
+ # As alternatives, it would be more intuitive to explicitly raise an
13
+ # error when rollback is desired, and to use `next` when commit is
14
+ # desired.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # ApplicationRecord.transaction do
19
+ # return if user.active?
20
+ # end
21
+ #
22
+ # # bad
23
+ # ApplicationRecord.transaction do
24
+ # break if user.active?
25
+ # end
26
+ #
27
+ # # bad
28
+ # ApplicationRecord.transaction do
29
+ # throw if user.active?
30
+ # end
31
+ #
32
+ # # good
33
+ # ApplicationRecord.transaction do
34
+ # # Rollback
35
+ # raise "User is active" if user.active?
36
+ # end
37
+ #
38
+ # # good
39
+ # ApplicationRecord.transaction do
40
+ # # Commit
41
+ # next if user.active?
42
+ # end
43
+ #
44
+ # @see https://github.com/rails/rails/commit/15aa4200e083
45
+ class TransactionExitStatement < Base
46
+ MSG = <<~MSG.chomp
47
+ Exit statement `%<statement>s` is not allowed. Use `raise` (rollback) or `next` (commit).
48
+ MSG
49
+
50
+ RESTRICT_ON_SEND = %i[transaction].freeze
51
+
52
+ def_node_search :exit_statements, <<~PATTERN
53
+ ({return | break | send nil? :throw} ...)
54
+ PATTERN
55
+
56
+ def on_send(node)
57
+ parent = node.parent
58
+
59
+ return unless parent&.block_type?
60
+
61
+ exit_statements(parent.body).each do |statement_node|
62
+ statement = if statement_node.return_type?
63
+ 'return'
64
+ elsif statement_node.break_type?
65
+ 'break'
66
+ else
67
+ statement_node.method_name
68
+ end
69
+ message = format(MSG, statement: statement)
70
+
71
+ add_offense(statement_node, message: message)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require_relative 'mixin/active_record_helper'
4
4
  require_relative 'mixin/active_record_migrations_helper'
5
+ require_relative 'mixin/class_send_node_helper'
5
6
  require_relative 'mixin/enforce_superclass'
6
7
  require_relative 'mixin/index_method'
7
8
  require_relative 'mixin/migrations_helper'
8
9
  require_relative 'mixin/target_rails_version'
9
10
 
11
+ require_relative 'rails/action_controller_test_case'
10
12
  require_relative 'rails/action_filter'
11
13
  require_relative 'rails/active_record_aliases'
12
14
  require_relative 'rails/active_record_callbacks_order'
@@ -31,6 +33,9 @@ require_relative 'rails/date'
31
33
  require_relative 'rails/default_scope'
32
34
  require_relative 'rails/delegate'
33
35
  require_relative 'rails/delegate_allow_blank'
36
+ require_relative 'rails/deprecated_active_model_errors_methods'
37
+ require_relative 'rails/duplicate_association'
38
+ require_relative 'rails/duplicate_scope'
34
39
  require_relative 'rails/duration_arithmetic'
35
40
  require_relative 'rails/dynamic_find_by'
36
41
  require_relative 'rails/eager_evaluation_log_message'
@@ -49,7 +54,9 @@ require_relative 'rails/has_many_or_has_one_dependent'
49
54
  require_relative 'rails/helper_instance_variable'
50
55
  require_relative 'rails/http_positional_arguments'
51
56
  require_relative 'rails/http_status'
57
+ require_relative 'rails/i18n_lazy_lookup'
52
58
  require_relative 'rails/i18n_locale_assignment'
59
+ require_relative 'rails/i18n_locale_texts'
53
60
  require_relative 'rails/ignored_skip_action_filter_option'
54
61
  require_relative 'rails/index_by'
55
62
  require_relative 'rails/index_with'
@@ -59,6 +66,7 @@ require_relative 'rails/lexically_scoped_action_filter'
59
66
  require_relative 'rails/link_to_blank'
60
67
  require_relative 'rails/mailer_name'
61
68
  require_relative 'rails/match_route'
69
+ require_relative 'rails/migration_class_name'
62
70
  require_relative 'rails/negate_include'
63
71
  require_relative 'rails/not_null_column'
64
72
  require_relative 'rails/order_by_id'
@@ -96,8 +104,10 @@ require_relative 'rails/scope_args'
96
104
  require_relative 'rails/short_i18n'
97
105
  require_relative 'rails/skips_model_validations'
98
106
  require_relative 'rails/squished_sql_heredocs'
107
+ require_relative 'rails/table_name_assignment'
99
108
  require_relative 'rails/time_zone'
100
109
  require_relative 'rails/time_zone_assignment'
110
+ require_relative 'rails/transaction_exit_statement'
101
111
  require_relative 'rails/uniq_before_pluck'
102
112
  require_relative 'rails/unique_validation_without_index'
103
113
  require_relative 'rails/unknown_env'
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.13.2'
7
+ STRING = '2.14.0'
8
8
 
9
9
  def self.document_version
10
10
  STRING.match('\d+\.\d+').to_s
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.2
4
+ version: 2.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bozhidar Batsov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-01-15 00:00:00.000000000 Z
13
+ date: 2022-03-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -79,10 +79,12 @@ files:
79
79
  - lib/rubocop-rails.rb
80
80
  - lib/rubocop/cop/mixin/active_record_helper.rb
81
81
  - lib/rubocop/cop/mixin/active_record_migrations_helper.rb
82
+ - lib/rubocop/cop/mixin/class_send_node_helper.rb
82
83
  - lib/rubocop/cop/mixin/enforce_superclass.rb
83
84
  - lib/rubocop/cop/mixin/index_method.rb
84
85
  - lib/rubocop/cop/mixin/migrations_helper.rb
85
86
  - lib/rubocop/cop/mixin/target_rails_version.rb
87
+ - lib/rubocop/cop/rails/action_controller_test_case.rb
86
88
  - lib/rubocop/cop/rails/action_filter.rb
87
89
  - lib/rubocop/cop/rails/active_record_aliases.rb
88
90
  - lib/rubocop/cop/rails/active_record_callbacks_order.rb
@@ -107,6 +109,9 @@ files:
107
109
  - lib/rubocop/cop/rails/default_scope.rb
108
110
  - lib/rubocop/cop/rails/delegate.rb
109
111
  - lib/rubocop/cop/rails/delegate_allow_blank.rb
112
+ - lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb
113
+ - lib/rubocop/cop/rails/duplicate_association.rb
114
+ - lib/rubocop/cop/rails/duplicate_scope.rb
110
115
  - lib/rubocop/cop/rails/duration_arithmetic.rb
111
116
  - lib/rubocop/cop/rails/dynamic_find_by.rb
112
117
  - lib/rubocop/cop/rails/eager_evaluation_log_message.rb
@@ -125,7 +130,9 @@ files:
125
130
  - lib/rubocop/cop/rails/helper_instance_variable.rb
126
131
  - lib/rubocop/cop/rails/http_positional_arguments.rb
127
132
  - lib/rubocop/cop/rails/http_status.rb
133
+ - lib/rubocop/cop/rails/i18n_lazy_lookup.rb
128
134
  - lib/rubocop/cop/rails/i18n_locale_assignment.rb
135
+ - lib/rubocop/cop/rails/i18n_locale_texts.rb
129
136
  - lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb
130
137
  - lib/rubocop/cop/rails/index_by.rb
131
138
  - lib/rubocop/cop/rails/index_with.rb
@@ -135,6 +142,7 @@ files:
135
142
  - lib/rubocop/cop/rails/link_to_blank.rb
136
143
  - lib/rubocop/cop/rails/mailer_name.rb
137
144
  - lib/rubocop/cop/rails/match_route.rb
145
+ - lib/rubocop/cop/rails/migration_class_name.rb
138
146
  - lib/rubocop/cop/rails/negate_include.rb
139
147
  - lib/rubocop/cop/rails/not_null_column.rb
140
148
  - lib/rubocop/cop/rails/order_by_id.rb
@@ -172,8 +180,10 @@ files:
172
180
  - lib/rubocop/cop/rails/short_i18n.rb
173
181
  - lib/rubocop/cop/rails/skips_model_validations.rb
174
182
  - lib/rubocop/cop/rails/squished_sql_heredocs.rb
183
+ - lib/rubocop/cop/rails/table_name_assignment.rb
175
184
  - lib/rubocop/cop/rails/time_zone.rb
176
185
  - lib/rubocop/cop/rails/time_zone_assignment.rb
186
+ - lib/rubocop/cop/rails/transaction_exit_statement.rb
177
187
  - lib/rubocop/cop/rails/uniq_before_pluck.rb
178
188
  - lib/rubocop/cop/rails/unique_validation_without_index.rb
179
189
  - lib/rubocop/cop/rails/unknown_env.rb
@@ -195,7 +205,7 @@ metadata:
195
205
  homepage_uri: https://docs.rubocop.org/rubocop-rails/
196
206
  changelog_uri: https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md
197
207
  source_code_uri: https://github.com/rubocop/rubocop-rails/
198
- documentation_uri: https://docs.rubocop.org/rubocop-rails/2.13/
208
+ documentation_uri: https://docs.rubocop.org/rubocop-rails/2.14/
199
209
  bug_tracker_uri: https://github.com/rubocop/rubocop-rails/issues
200
210
  rubygems_mfa_required: 'true'
201
211
  post_install_message: