rubocop-rails 2.13.1 → 2.14.1

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.
@@ -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
@@ -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,71 @@
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 `%<corrected_class_name>s` that matches the file name.'
26
+
27
+ def on_class(node)
28
+ return if in_migration?(node)
29
+
30
+ snake_class_name = to_snakecase(node.identifier.source)
31
+
32
+ basename = basename_without_timestamp_and_suffix
33
+ return if snake_class_name == basename
34
+
35
+ corrected_class_name = to_camelcase(basename)
36
+ message = format(MSG, corrected_class_name: corrected_class_name)
37
+
38
+ add_offense(node.identifier, message: message) do |corrector|
39
+ corrector.replace(node.identifier, corrected_class_name)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def basename_without_timestamp_and_suffix
46
+ filepath = processed_source.file_path
47
+ basename = File.basename(filepath, '.rb')
48
+ basename = remove_gem_suffix(basename)
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 to_camelcase(word)
58
+ word.split('_').map(&:capitalize).join
59
+ end
60
+
61
+ def to_snakecase(word)
62
+ word
63
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
64
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
65
+ .tr('-', '_')
66
+ .downcase
67
+ end
68
+ end
69
+ end
70
+ end
71
+ 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
@@ -37,7 +37,7 @@ module RuboCop
37
37
  class ReadWriteAttribute < Base
38
38
  extend AutoCorrector
39
39
 
40
- MSG = 'Prefer `%<prefer>s` over `%<current>s`.'
40
+ MSG = 'Prefer `%<prefer>s`.'
41
41
  RESTRICT_ON_SEND = %i[read_attribute write_attribute].freeze
42
42
 
43
43
  def_node_matcher :read_write_attribute?, <<~PATTERN
@@ -51,35 +51,51 @@ module RuboCop
51
51
  return unless read_write_attribute?(node)
52
52
  return if within_shadowing_method?(node)
53
53
 
54
- add_offense(node.loc.selector, message: message(node)) do |corrector|
55
- case node.method_name
56
- when :read_attribute
57
- replacement = read_attribute_replacement(node)
58
- when :write_attribute
59
- replacement = write_attribute_replacement(node)
60
- end
61
-
62
- 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))
63
56
  end
64
57
  end
65
58
 
66
59
  private
67
60
 
68
61
  def within_shadowing_method?(node)
69
- node.each_ancestor(:def).any? do |enclosing_method|
70
- shadowing_method_name = node.first_argument.value.to_s
71
- shadowing_method_name << '=' if node.method?(:write_attribute)
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
72
67
 
73
- enclosing_method.method_name.to_s == shadowing_method_name
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)
74
78
  end
75
79
  end
76
80
 
77
- def message(node)
81
+ def single_line_message(node)
82
+ format(MSG, prefer: node_replacement(node))
83
+ end
84
+
85
+ def multi_line_message(node)
78
86
  if node.method?(:read_attribute)
79
- format(MSG, prefer: 'self[:attr]', current: 'read_attribute(:attr)')
87
+ format(MSG, prefer: 'self[:attr]')
80
88
  else
81
- format(MSG, prefer: 'self[:attr] = val',
82
- 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)
83
99
  end
84
100
  end
85
101
 
@@ -8,6 +8,10 @@ module RuboCop
8
8
  # explicitly set to `false`. The presence validator is added
9
9
  # automatically, and explicit presence validation is redundant.
10
10
  #
11
+ # @safety
12
+ # This cop's autocorrection is unsafe because it changes the default error message
13
+ # from "can't be blank" to "must exist".
14
+ #
11
15
  # @example
12
16
  # # bad
13
17
  # belongs_to :user
@@ -46,9 +50,6 @@ module RuboCop
46
50
  # @example source that matches - by association
47
51
  # validates :name, :user, presence: true
48
52
  #
49
- # @example source that matches - with presence options
50
- # validates :user, presence: { message: 'duplicate' }
51
- #
52
53
  # @example source that matches - by a foreign key
53
54
  # validates :user_id, presence: true
54
55
  #
@@ -62,7 +63,7 @@ module RuboCop
62
63
  send nil? :validates
63
64
  (sym $_)+
64
65
  $[
65
- (hash <$(pair (sym :presence) {true hash}) ...>) # presence: true
66
+ (hash <$(pair (sym :presence) true) ...>) # presence: true
66
67
  !(hash <$(pair (sym :strict) {true const}) ...>) # strict: true
67
68
  ]
68
69
  )
@@ -176,6 +176,8 @@ module RuboCop
176
176
  #
177
177
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
178
178
  class ReversibleMigration < Base
179
+ include MigrationsHelper
180
+
179
181
  MSG = '%<action>s is not reversible.'
180
182
 
181
183
  def_node_matcher :irreversible_schema_statement_call, <<~PATTERN
@@ -207,7 +209,7 @@ module RuboCop
207
209
  PATTERN
208
210
 
209
211
  def on_send(node)
210
- return unless within_change_method?(node)
212
+ return unless in_migration?(node) && within_change_method?(node)
211
213
  return if within_reversible_or_up_only_block?(node)
212
214
 
213
215
  check_irreversible_schema_statement_node(node)
@@ -220,7 +222,7 @@ module RuboCop
220
222
  end
221
223
 
222
224
  def on_block(node)
223
- return unless within_change_method?(node)
225
+ return unless in_migration?(node) && within_change_method?(node)
224
226
  return if within_reversible_or_up_only_block?(node)
225
227
  return if node.body.nil?
226
228
 
@@ -43,19 +43,11 @@ module RuboCop
43
43
  # end
44
44
  # end
45
45
  class ReversibleMigrationMethodDefinition < Base
46
+ include MigrationsHelper
47
+
46
48
  MSG = 'Migrations must contain either a `change` method, or ' \
47
49
  'both an `up` and a `down` method.'
48
50
 
49
- def_node_matcher :migration_class?, <<~PATTERN
50
- (class
51
- (const nil? _)
52
- (send
53
- (const (const {nil? cbase} :ActiveRecord) :Migration)
54
- :[]
55
- (float _))
56
- _)
57
- PATTERN
58
-
59
51
  def_node_matcher :change_method?, <<~PATTERN
60
52
  [ #migration_class? `(def :change (args) _) ]
61
53
  PATTERN
@@ -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