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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- 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/index_by.rb +6 -6
- data/lib/rubocop/cop/rails/index_with.rb +6 -6
- data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
- data/lib/rubocop/cop/rails/migration_class_name.rb +61 -0
- data/lib/rubocop/cop/rails/pluck.rb +15 -7
- data/lib/rubocop/cop/rails/read_write_attribute.rb +51 -14
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +93 -28
- data/lib/rubocop/cop/rails/reversible_migration.rb +5 -3
- 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/unused_ignored_columns.rb +2 -0
- data/lib/rubocop/cop/rails_cops.rb +11 -0
- data/lib/rubocop/rails/version.rb +1 -1
- 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
|
-
(
|
26
|
+
(call _ :each_with_object (hash))
|
27
27
|
(args (arg $_el) (arg _memo))
|
28
|
-
(
|
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
|
-
(
|
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
|
-
(
|
39
|
+
(call
|
40
40
|
(block
|
41
|
-
(
|
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
|
-
(
|
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
|
-
(
|
29
|
+
(call _ :each_with_object (hash))
|
30
30
|
(args (arg $_el) (arg _memo))
|
31
|
-
(
|
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
|
-
(
|
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
|
-
(
|
42
|
+
(call
|
43
43
|
(block
|
44
|
-
(
|
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
|
-
(
|
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 `%<
|
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
|
@@ -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
|
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
|
43
|
-
|
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
|
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]'
|
87
|
+
format(MSG, prefer: 'self[:attr]')
|
59
88
|
else
|
60
|
-
format(MSG, prefer: 'self[:attr] = val'
|
61
|
-
|
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
|
|