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
@@ -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
@@ -23,22 +23,22 @@ module RuboCop
23
23
 
24
24
  def_node_matcher :on_bad_each_with_object, <<~PATTERN
25
25
  (block
26
- ({send csend} _ :each_with_object (hash))
26
+ (call _ :each_with_object (hash))
27
27
  (args (arg $_el) (arg _memo))
28
- ({send csend} (lvar _memo) :[]= $!`_memo (lvar _el)))
28
+ (call (lvar _memo) :[]= $!`_memo (lvar _el)))
29
29
  PATTERN
30
30
 
31
31
  def_node_matcher :on_bad_to_h, <<~PATTERN
32
32
  (block
33
- ({send csend} _ :to_h)
33
+ (call _ :to_h)
34
34
  (args (arg $_el))
35
35
  (array $_ (lvar _el)))
36
36
  PATTERN
37
37
 
38
38
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
39
- ({send csend}
39
+ (call
40
40
  (block
41
- ({send csend} _ {:map :collect})
41
+ (call _ {:map :collect})
42
42
  (args (arg $_el))
43
43
  (array $_ (lvar _el)))
44
44
  :to_h)
@@ -49,7 +49,7 @@ module RuboCop
49
49
  (const _ :Hash)
50
50
  :[]
51
51
  (block
52
- ({send csend} _ {:map :collect})
52
+ (call _ {:map :collect})
53
53
  (args (arg $_el))
54
54
  (array $_ (lvar _el))))
55
55
  PATTERN
@@ -26,22 +26,22 @@ module RuboCop
26
26
 
27
27
  def_node_matcher :on_bad_each_with_object, <<~PATTERN
28
28
  (block
29
- ({send csend} _ :each_with_object (hash))
29
+ (call _ :each_with_object (hash))
30
30
  (args (arg $_el) (arg _memo))
31
- ({send csend} (lvar _memo) :[]= (lvar _el) $!`_memo))
31
+ (call (lvar _memo) :[]= (lvar _el) $!`_memo))
32
32
  PATTERN
33
33
 
34
34
  def_node_matcher :on_bad_to_h, <<~PATTERN
35
35
  (block
36
- ({send csend} _ :to_h)
36
+ (call _ :to_h)
37
37
  (args (arg $_el))
38
38
  (array (lvar _el) $_))
39
39
  PATTERN
40
40
 
41
41
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
42
- ({send csend}
42
+ (call
43
43
  (block
44
- ({send csend} _ {:map :collect})
44
+ (call _ {:map :collect})
45
45
  (args (arg $_el))
46
46
  (array (lvar _el) $_))
47
47
  :to_h)
@@ -52,7 +52,7 @@ module RuboCop
52
52
  (const _ :Hash)
53
53
  :[]
54
54
  (block
55
- ({send csend} _ {:map :collect})
55
+ (call _ {:map :collect})
56
56
  (args (arg $_el))
57
57
  (array (lvar _el) $_)))
58
58
  PATTERN
@@ -126,6 +126,18 @@ module RuboCop
126
126
  # has_many :physicians, through: :appointments
127
127
  # end
128
128
  #
129
+ # @example IgnoreScopes: false (default)
130
+ # # bad
131
+ # class Blog < ApplicationRecord
132
+ # has_many :posts, -> { order(published_at: :desc) }
133
+ # end
134
+ #
135
+ # @example IgnoreScopes: true
136
+ # # good
137
+ # class Blog < ApplicationRecord
138
+ # has_many :posts, -> { order(published_at: :desc) }
139
+ # end
140
+ #
129
141
  # @see https://guides.rubyonrails.org/association_basics.html#bi-directional-associations
130
142
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
131
143
  class InverseOf < Base
@@ -189,7 +201,7 @@ module RuboCop
189
201
  end
190
202
 
191
203
  def scope?(arguments)
192
- arguments.any?(&:block_type?)
204
+ !ignore_scopes? && arguments.any?(&:block_type?)
193
205
  end
194
206
 
195
207
  def options_requiring_inverse_of?(options)
@@ -236,6 +248,10 @@ module RuboCop
236
248
  SPECIFY_MSG
237
249
  end
238
250
  end
251
+
252
+ def ignore_scopes?
253
+ cop_config['IgnoreScopes'] == true
254
+ end
239
255
  end
240
256
  end
241
257
  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
@@ -23,10 +23,21 @@ module RuboCop
23
23
  # # good
24
24
  # x = self[:attr]
25
25
  # self[:attr] = val
26
+ #
27
+ # When called from within a method with the same name as the attribute,
28
+ # `read_attribute` and `write_attribute` must be used to prevent an
29
+ # infinite loop:
30
+ #
31
+ # @example
32
+ #
33
+ # # good
34
+ # def foo
35
+ # bar || read_attribute(:foo)
36
+ # end
26
37
  class ReadWriteAttribute < Base
27
38
  extend AutoCorrector
28
39
 
29
- MSG = 'Prefer `%<prefer>s` over `%<current>s`.'
40
+ MSG = 'Prefer `%<prefer>s`.'
30
41
  RESTRICT_ON_SEND = %i[read_attribute write_attribute].freeze
31
42
 
32
43
  def_node_matcher :read_write_attribute?, <<~PATTERN
@@ -38,27 +49,53 @@ module RuboCop
38
49
 
39
50
  def on_send(node)
40
51
  return unless read_write_attribute?(node)
52
+ return if within_shadowing_method?(node)
41
53
 
42
- add_offense(node.loc.selector, message: message(node)) do |corrector|
43
- case node.method_name
44
- when :read_attribute
45
- replacement = read_attribute_replacement(node)
46
- when :write_attribute
47
- replacement = write_attribute_replacement(node)
48
- end
49
-
50
- corrector.replace(node.source_range, replacement)
54
+ add_offense(node, message: build_message(node)) do |corrector|
55
+ corrector.replace(node.source_range, node_replacement(node))
51
56
  end
52
57
  end
53
58
 
54
59
  private
55
60
 
56
- def message(node)
61
+ def within_shadowing_method?(node)
62
+ first_arg = node.first_argument
63
+ return false unless first_arg.respond_to?(:value)
64
+
65
+ enclosing_method = node.each_ancestor(:def).first
66
+ return false unless enclosing_method
67
+
68
+ shadowing_method_name = first_arg.value.to_s
69
+ shadowing_method_name << '=' if node.method?(:write_attribute)
70
+ enclosing_method.method?(shadowing_method_name)
71
+ end
72
+
73
+ def build_message(node)
74
+ if node.single_line?
75
+ single_line_message(node)
76
+ else
77
+ multi_line_message(node)
78
+ end
79
+ end
80
+
81
+ def single_line_message(node)
82
+ format(MSG, prefer: node_replacement(node))
83
+ end
84
+
85
+ def multi_line_message(node)
57
86
  if node.method?(:read_attribute)
58
- format(MSG, prefer: 'self[:attr]', current: 'read_attribute(:attr)')
87
+ format(MSG, prefer: 'self[:attr]')
59
88
  else
60
- format(MSG, prefer: 'self[:attr] = val',
61
- current: 'write_attribute(:attr, val)')
89
+ format(MSG, prefer: 'self[:attr] = val')
90
+ end
91
+ end
92
+
93
+ def node_replacement(node)
94
+ case node.method_name
95
+ when :read_attribute
96
+ read_attribute_replacement(node)
97
+ when :write_attribute
98
+ write_attribute_replacement(node)
62
99
  end
63
100
  end
64
101