rubocop-rails 2.13.1 → 2.14.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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