rubocop-rails 2.13.2 → 2.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87ab88a596e8f8dbe2ef0ded9a93ae1ceba10572ad41fc6b4e1b0b684cf68110
4
- data.tar.gz: 4bc8502424aa32efec36f06ca475b7a3918b160060f76aa65eba39bfa5a31947
3
+ metadata.gz: babf21479a37aa37cba8b6b8401b23ddb6532e14afd679095d456f749aca3ca0
4
+ data.tar.gz: 11a94f8219a5b9d241d68af1c3ecd69afb0a47ccf988db59d00f48caaf73dfbf
5
5
  SHA512:
6
- metadata.gz: 27016cd823ac6eb75ddb06f97c613fea178b29dc8cadac6289c803a9745ccbe2ffd7f7799c82b1a9ddd74f08ab162fcb50acf9a491a5841f1d09931eec8145fa
7
- data.tar.gz: 95c826339433a0a87f296676216e62b493cdc4d5b67ac73c400659194d3a54e874554d0431c3cbea02316d4265cd870755320ed5f5fd6b42722fdd17feba041e
6
+ metadata.gz: 02b2646080a54c50c8708cd6feeed501b5aafd7ae77d04e42ed0bf5b8779f43fa778bc3f46daec2c06474ac76f40ae144bf0afed35e5acf996c4b2213df53f89
7
+ data.tar.gz: 4b29b44608474ead97651327e5c896bfb403eadcc1deffd10730853f303e75682e3ebebed80f8b3b05c1f9f0862441845bea89135d694ecf70ee3d6c776f2a97
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,63 @@
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
+ include MigrationsHelper
24
+
25
+ MSG = 'Replace with `%<camelized_basename>s` that matches the file name.'
26
+
27
+ def on_class(node)
28
+ return unless migration_class?(node)
29
+
30
+ basename = basename_without_timestamp_and_suffix(processed_source.file_path)
31
+
32
+ class_identifier = node.identifier
33
+ camelized_basename = camelize(basename)
34
+ return if class_identifier.source.casecmp(camelized_basename).zero?
35
+
36
+ message = format(MSG, camelized_basename: camelized_basename)
37
+
38
+ add_offense(class_identifier, message: message) do |corrector|
39
+ corrector.replace(class_identifier, camelized_basename)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def basename_without_timestamp_and_suffix(filepath)
46
+ basename = File.basename(filepath, '.rb')
47
+ basename = remove_gem_suffix(basename)
48
+
49
+ basename.sub(/\A\d+_/, '')
50
+ end
51
+
52
+ # e.g.: from `add_blobs.active_storage` to `add_blobs`.
53
+ def remove_gem_suffix(file_name)
54
+ file_name.sub(/\..+\z/, '')
55
+ end
56
+
57
+ def camelize(word)
58
+ word.split('_').map(&:capitalize).join
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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,94 @@
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
+ return unless (parent = node.parent)
58
+ return unless parent.block_type? && parent.body
59
+
60
+ exit_statements(parent.body).each do |statement_node|
61
+ next if in_rescue?(statement_node) || nested_block?(statement_node)
62
+
63
+ statement = statement(statement_node)
64
+ message = format(MSG, statement: statement)
65
+
66
+ add_offense(statement_node, message: message)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def statement(statement_node)
73
+ if statement_node.return_type?
74
+ 'return'
75
+ elsif statement_node.break_type?
76
+ 'break'
77
+ else
78
+ statement_node.method_name
79
+ end
80
+ end
81
+
82
+ def in_rescue?(statement_node)
83
+ statement_node.ancestors.find(&:rescue_type?)
84
+ end
85
+
86
+ def nested_block?(statement_node)
87
+ return false unless statement_node.break_type?
88
+
89
+ !statement_node.ancestors.find(&:block_type?).method?(:transaction)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ 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.2'
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.2
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-18 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: