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.
- checksums.yaml +4 -4
- data/config/default.yml +75 -4
- data/lib/rubocop/cop/mixin/class_send_node_helper.rb +20 -0
- data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
- data/lib/rubocop/cop/rails/action_controller_test_case.rb +47 -0
- data/lib/rubocop/cop/rails/after_commit_override.rb +2 -12
- data/lib/rubocop/cop/rails/bulk_change_table.rb +20 -6
- data/lib/rubocop/cop/rails/compact_blank.rb +22 -13
- data/lib/rubocop/cop/rails/deprecated_active_model_errors_methods.rb +108 -0
- data/lib/rubocop/cop/rails/duplicate_association.rb +56 -0
- data/lib/rubocop/cop/rails/duplicate_scope.rb +46 -0
- data/lib/rubocop/cop/rails/duration_arithmetic.rb +2 -1
- data/lib/rubocop/cop/rails/i18n_lazy_lookup.rb +94 -0
- data/lib/rubocop/cop/rails/i18n_locale_texts.rb +110 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
- data/lib/rubocop/cop/rails/migration_class_name.rb +71 -0
- data/lib/rubocop/cop/rails/pluck.rb +15 -7
- data/lib/rubocop/cop/rails/read_write_attribute.rb +34 -18
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +5 -4
- data/lib/rubocop/cop/rails/reversible_migration.rb +4 -2
- data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +2 -10
- data/lib/rubocop/cop/rails/table_name_assignment.rb +44 -0
- data/lib/rubocop/cop/rails/transaction_exit_statement.rb +77 -0
- data/lib/rubocop/cop/rails_cops.rb +11 -0
- data/lib/rubocop/rails/version.rb +1 -1
- metadata +14 -3
@@ -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 `%<
|
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 _
|
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 |
|
34
|
-
|
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(
|
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(
|
51
|
-
|
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
|
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
|
55
|
-
|
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.
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
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]'
|
87
|
+
format(MSG, prefer: 'self[:attr]')
|
80
88
|
else
|
81
|
-
format(MSG, prefer: 'self[:attr] = val'
|
82
|
-
|
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)
|
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
|