rubocop-rails 2.4.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 +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +92 -0
- data/bin/setup +7 -0
- data/config/default.yml +510 -0
- data/lib/rubocop-rails.rb +12 -0
- data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
- data/lib/rubocop/cop/rails/action_filter.rb +111 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
- data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
- data/lib/rubocop/cop/rails/application_controller.rb +36 -0
- data/lib/rubocop/cop/rails/application_job.rb +40 -0
- data/lib/rubocop/cop/rails/application_mailer.rb +40 -0
- data/lib/rubocop/cop/rails/application_record.rb +40 -0
- data/lib/rubocop/cop/rails/assert_not.rb +44 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
- data/lib/rubocop/cop/rails/blank.rb +164 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +293 -0
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
- data/lib/rubocop/cop/rails/date.rb +161 -0
- data/lib/rubocop/cop/rails/delegate.rb +132 -0
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
- data/lib/rubocop/cop/rails/enum_hash.rb +75 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +65 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
- data/lib/rubocop/cop/rails/exit.rb +67 -0
- data/lib/rubocop/cop/rails/file_path.rb +108 -0
- data/lib/rubocop/cop/rails/find_by.rb +55 -0
- data/lib/rubocop/cop/rails/find_each.rb +51 -0
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
- data/lib/rubocop/cop/rails/http_status.rb +160 -0
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
- data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
- data/lib/rubocop/cop/rails/output.rb +49 -0
- data/lib/rubocop/cop/rails/output_safety.rb +99 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
- data/lib/rubocop/cop/rails/presence.rb +148 -0
- data/lib/rubocop/cop/rails/present.rb +153 -0
- data/lib/rubocop/cop/rails/rake_environment.rb +91 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
- data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
- data/lib/rubocop/cop/rails/relative_date_constant.rb +102 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +284 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +85 -0
- data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +48 -0
- data/lib/rubocop/cop/rails/save_bang.rb +331 -0
- data/lib/rubocop/cop/rails/scope_args.rb +29 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
- data/lib/rubocop/cop/rails/time_zone.rb +249 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +84 -0
- data/lib/rubocop/cop/rails/validation.rb +147 -0
- data/lib/rubocop/cop/rails_cops.rb +61 -0
- data/lib/rubocop/rails.rb +12 -0
- data/lib/rubocop/rails/inject.rb +18 -0
- data/lib/rubocop/rails/version.rb +10 -0
- metadata +148 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop converts usages of `try!` to `&.`. It can also be configured
|
7
|
+
# to convert `try`. It will convert code to use safe navigation.
|
8
|
+
#
|
9
|
+
# @example ConvertTry: false (default)
|
10
|
+
# # bad
|
11
|
+
# foo.try!(:bar)
|
12
|
+
# foo.try!(:bar, baz)
|
13
|
+
# foo.try!(:bar) { |e| e.baz }
|
14
|
+
#
|
15
|
+
# foo.try!(:[], 0)
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# foo.try(:bar)
|
19
|
+
# foo.try(:bar, baz)
|
20
|
+
# foo.try(:bar) { |e| e.baz }
|
21
|
+
#
|
22
|
+
# foo&.bar
|
23
|
+
# foo&.bar(baz)
|
24
|
+
# foo&.bar { |e| e.baz }
|
25
|
+
#
|
26
|
+
# @example ConvertTry: true
|
27
|
+
# # bad
|
28
|
+
# foo.try!(:bar)
|
29
|
+
# foo.try!(:bar, baz)
|
30
|
+
# foo.try!(:bar) { |e| e.baz }
|
31
|
+
# foo.try(:bar)
|
32
|
+
# foo.try(:bar, baz)
|
33
|
+
# foo.try(:bar) { |e| e.baz }
|
34
|
+
#
|
35
|
+
# # good
|
36
|
+
# foo&.bar
|
37
|
+
# foo&.bar(baz)
|
38
|
+
# foo&.bar { |e| e.baz }
|
39
|
+
class SafeNavigation < Cop
|
40
|
+
include RangeHelp
|
41
|
+
|
42
|
+
MSG = 'Use safe navigation (`&.`) instead of `%<try>s`.'
|
43
|
+
|
44
|
+
def_node_matcher :try_call, <<~PATTERN
|
45
|
+
(send !nil? ${:try :try!} $_ ...)
|
46
|
+
PATTERN
|
47
|
+
|
48
|
+
def on_send(node)
|
49
|
+
try_call(node) do |try_method, dispatch|
|
50
|
+
return if try_method == :try && !cop_config['ConvertTry']
|
51
|
+
return unless dispatch.sym_type? && dispatch.value =~ /\w+[=!?]?/
|
52
|
+
|
53
|
+
add_offense(node, message: format(MSG, try: try_method))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def autocorrect(node)
|
58
|
+
method_node, *params = *node.arguments
|
59
|
+
method = method_node.source[1..-1]
|
60
|
+
|
61
|
+
range = range_between(node.loc.dot.begin_pos,
|
62
|
+
node.loc.expression.end_pos)
|
63
|
+
|
64
|
+
lambda do |corrector|
|
65
|
+
corrector.replace(range, replacement(method, params))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def replacement(method, params)
|
72
|
+
new_params = params.map(&:source).join(', ')
|
73
|
+
|
74
|
+
if method.end_with?('=')
|
75
|
+
"&.#{method[0...-1]} = #{new_params}"
|
76
|
+
elsif params.empty?
|
77
|
+
"&.#{method}"
|
78
|
+
else
|
79
|
+
"&.#{method}(#{new_params})"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks to make sure safe navigation isn't used with `blank?` in
|
7
|
+
# a conditional.
|
8
|
+
#
|
9
|
+
# While the safe navigation operator is generally a good idea, when
|
10
|
+
# checking `foo&.blank?` in a conditional, `foo` being `nil` will actually
|
11
|
+
# do the opposite of what the author intends.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# do_something if foo&.blank?
|
16
|
+
# do_something unless foo&.blank?
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# do_something if foo.blank?
|
20
|
+
# do_something unless foo.blank?
|
21
|
+
#
|
22
|
+
class SafeNavigationWithBlank < Cop
|
23
|
+
MSG =
|
24
|
+
'Avoid calling `blank?` with the safe navigation operator ' \
|
25
|
+
'in conditionals.'
|
26
|
+
|
27
|
+
def_node_matcher :safe_navigation_blank_in_conditional?, <<~PATTERN
|
28
|
+
(if $(csend ... :blank?) ...)
|
29
|
+
PATTERN
|
30
|
+
|
31
|
+
def on_if(node)
|
32
|
+
return unless safe_navigation_blank_in_conditional?(node)
|
33
|
+
|
34
|
+
add_offense(node)
|
35
|
+
end
|
36
|
+
|
37
|
+
def autocorrect(node)
|
38
|
+
lambda do |corrector|
|
39
|
+
corrector.replace(
|
40
|
+
safe_navigation_blank_in_conditional?(node).location.dot,
|
41
|
+
'.'
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop identifies possible cases where Active Record save! or related
|
7
|
+
# should be used instead of save because the model might have failed to
|
8
|
+
# save and an exception is better than unhandled failure.
|
9
|
+
#
|
10
|
+
# This will allow:
|
11
|
+
#
|
12
|
+
# - update or save calls, assigned to a variable,
|
13
|
+
# or used as a condition in an if/unless/case statement.
|
14
|
+
# - create calls, assigned to a variable that then has a
|
15
|
+
# call to `persisted?`.
|
16
|
+
# - calls if the result is explicitly returned from methods and blocks,
|
17
|
+
# or provided as arguments.
|
18
|
+
# - calls whose signature doesn't look like an ActiveRecord
|
19
|
+
# persistence method.
|
20
|
+
#
|
21
|
+
# By default it will also allow implicit returns from methods and blocks.
|
22
|
+
# that behavior can be turned off with `AllowImplicitReturn: false`.
|
23
|
+
#
|
24
|
+
# You can permit receivers that are giving false positives with
|
25
|
+
# `AllowedReceivers: []`
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
#
|
29
|
+
# # bad
|
30
|
+
# user.save
|
31
|
+
# user.update(name: 'Joe')
|
32
|
+
# user.find_or_create_by(name: 'Joe')
|
33
|
+
# user.destroy
|
34
|
+
#
|
35
|
+
# # good
|
36
|
+
# unless user.save
|
37
|
+
# # ...
|
38
|
+
# end
|
39
|
+
# user.save!
|
40
|
+
# user.update!(name: 'Joe')
|
41
|
+
# user.find_or_create_by!(name: 'Joe')
|
42
|
+
# user.destroy!
|
43
|
+
#
|
44
|
+
# user = User.find_or_create_by(name: 'Joe')
|
45
|
+
# unless user.persisted?
|
46
|
+
# # ...
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# def save_user
|
50
|
+
# return user.save
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example AllowImplicitReturn: true (default)
|
54
|
+
#
|
55
|
+
# # good
|
56
|
+
# users.each { |u| u.save }
|
57
|
+
#
|
58
|
+
# def save_user
|
59
|
+
# user.save
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example AllowImplicitReturn: false
|
63
|
+
#
|
64
|
+
# # bad
|
65
|
+
# users.each { |u| u.save }
|
66
|
+
# def save_user
|
67
|
+
# user.save
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # good
|
71
|
+
# users.each { |u| u.save! }
|
72
|
+
#
|
73
|
+
# def save_user
|
74
|
+
# user.save!
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# def save_user
|
78
|
+
# return user.save
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# @example AllowedReceivers: ['merchant.customers', 'Service::Mailer']
|
82
|
+
#
|
83
|
+
# # bad
|
84
|
+
# merchant.create
|
85
|
+
# customers.builder.save
|
86
|
+
# Mailer.create
|
87
|
+
#
|
88
|
+
# module Service::Mailer
|
89
|
+
# self.create
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# # good
|
93
|
+
# merchant.customers.create
|
94
|
+
# MerchantService.merchant.customers.destroy
|
95
|
+
# Service::Mailer.update(message: 'Message')
|
96
|
+
# ::Service::Mailer.update
|
97
|
+
# Services::Service::Mailer.update(message: 'Message')
|
98
|
+
# Service::Mailer::update
|
99
|
+
#
|
100
|
+
class SaveBang < Cop
|
101
|
+
include NegativeConditional
|
102
|
+
|
103
|
+
MSG = 'Use `%<prefer>s` instead of `%<current>s` if the return ' \
|
104
|
+
'value is not checked.'
|
105
|
+
CREATE_MSG = (MSG +
|
106
|
+
' Or check `persisted?` on model returned from ' \
|
107
|
+
'`%<current>s`.').freeze
|
108
|
+
CREATE_CONDITIONAL_MSG = '`%<current>s` returns a model which is ' \
|
109
|
+
'always truthy.'
|
110
|
+
|
111
|
+
CREATE_PERSIST_METHODS = %i[create create_or_find_by
|
112
|
+
first_or_create find_or_create_by].freeze
|
113
|
+
MODIFY_PERSIST_METHODS = %i[save
|
114
|
+
update update_attributes destroy].freeze
|
115
|
+
PERSIST_METHODS = (CREATE_PERSIST_METHODS +
|
116
|
+
MODIFY_PERSIST_METHODS).freeze
|
117
|
+
|
118
|
+
def join_force?(force_class)
|
119
|
+
force_class == VariableForce
|
120
|
+
end
|
121
|
+
|
122
|
+
def after_leaving_scope(scope, _variable_table)
|
123
|
+
scope.variables.each_value do |variable|
|
124
|
+
variable.assignments.each do |assignment|
|
125
|
+
check_assignment(assignment)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def check_assignment(assignment)
|
131
|
+
node = right_assignment_node(assignment)
|
132
|
+
|
133
|
+
return unless node&.send_type?
|
134
|
+
return unless persist_method?(node, CREATE_PERSIST_METHODS)
|
135
|
+
return if persisted_referenced?(assignment)
|
136
|
+
|
137
|
+
add_offense_for_node(node, CREATE_MSG)
|
138
|
+
end
|
139
|
+
|
140
|
+
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
|
141
|
+
return unless persist_method?(node)
|
142
|
+
return if return_value_assigned?(node)
|
143
|
+
return if implicit_return?(node)
|
144
|
+
return if check_used_in_condition_or_compound_boolean(node)
|
145
|
+
return if argument?(node)
|
146
|
+
return if explicit_return?(node)
|
147
|
+
|
148
|
+
add_offense_for_node(node)
|
149
|
+
end
|
150
|
+
alias on_csend on_send
|
151
|
+
|
152
|
+
def autocorrect(node)
|
153
|
+
save_loc = node.loc.selector
|
154
|
+
new_method = "#{node.method_name}!"
|
155
|
+
|
156
|
+
->(corrector) { corrector.replace(save_loc, new_method) }
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def add_offense_for_node(node, msg = MSG)
|
162
|
+
name = node.method_name
|
163
|
+
full_message = format(msg, prefer: "#{name}!", current: name.to_s)
|
164
|
+
|
165
|
+
add_offense(node, location: :selector, message: full_message)
|
166
|
+
end
|
167
|
+
|
168
|
+
def right_assignment_node(assignment)
|
169
|
+
node = assignment.node.child_nodes.first
|
170
|
+
|
171
|
+
return node unless node&.block_type?
|
172
|
+
|
173
|
+
node.send_node
|
174
|
+
end
|
175
|
+
|
176
|
+
def persisted_referenced?(assignment)
|
177
|
+
return unless assignment.referenced?
|
178
|
+
|
179
|
+
assignment.variable.references.any? do |reference|
|
180
|
+
call_to_persisted?(reference.node.parent)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def call_to_persisted?(node)
|
185
|
+
node.send_type? && node.method?(:persisted?)
|
186
|
+
end
|
187
|
+
|
188
|
+
def assignable_node(node)
|
189
|
+
assignable = node.block_node || node
|
190
|
+
while node
|
191
|
+
node = hash_parent(node) || array_parent(node)
|
192
|
+
assignable = node if node
|
193
|
+
end
|
194
|
+
assignable
|
195
|
+
end
|
196
|
+
|
197
|
+
def hash_parent(node)
|
198
|
+
pair = node.parent
|
199
|
+
return unless pair&.pair_type?
|
200
|
+
|
201
|
+
hash = pair.parent
|
202
|
+
return unless hash&.hash_type?
|
203
|
+
|
204
|
+
hash
|
205
|
+
end
|
206
|
+
|
207
|
+
def array_parent(node)
|
208
|
+
array = node.parent
|
209
|
+
return unless array&.array_type?
|
210
|
+
|
211
|
+
array
|
212
|
+
end
|
213
|
+
|
214
|
+
def check_used_in_condition_or_compound_boolean(node)
|
215
|
+
return false unless in_condition_or_compound_boolean?(node)
|
216
|
+
|
217
|
+
unless MODIFY_PERSIST_METHODS.include?(node.method_name)
|
218
|
+
add_offense_for_node(node, CREATE_CONDITIONAL_MSG)
|
219
|
+
end
|
220
|
+
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
def in_condition_or_compound_boolean?(node)
|
225
|
+
node = node.block_node || node
|
226
|
+
parent = node.parent
|
227
|
+
return false unless parent
|
228
|
+
|
229
|
+
operator_or_single_negative?(parent) ||
|
230
|
+
(conditional?(parent) && node == parent.condition)
|
231
|
+
end
|
232
|
+
|
233
|
+
def operator_or_single_negative?(node)
|
234
|
+
node.or_type? || node.and_type? || single_negative?(node)
|
235
|
+
end
|
236
|
+
|
237
|
+
def conditional?(parent)
|
238
|
+
parent.if_type? || parent.case_type?
|
239
|
+
end
|
240
|
+
|
241
|
+
def allowed_receiver?(node)
|
242
|
+
return false unless node.receiver
|
243
|
+
return false unless cop_config['AllowedReceivers']
|
244
|
+
|
245
|
+
cop_config['AllowedReceivers'].any? do |allowed_receiver|
|
246
|
+
receiver_chain_matches?(node, allowed_receiver)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def receiver_chain_matches?(node, allowed_receiver)
|
251
|
+
allowed_receiver.split('.').reverse.all? do |receiver_part|
|
252
|
+
node = node.receiver
|
253
|
+
return false unless node
|
254
|
+
|
255
|
+
if node.variable?
|
256
|
+
node.node_parts.first == receiver_part.to_sym
|
257
|
+
elsif node.send_type?
|
258
|
+
node.method?(receiver_part.to_sym)
|
259
|
+
elsif node.const_type?
|
260
|
+
const_matches?(node.const_name, receiver_part)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Const == Const
|
266
|
+
# ::Const == ::Const
|
267
|
+
# ::Const == Const
|
268
|
+
# Const == ::Const
|
269
|
+
# NameSpace::Const == Const
|
270
|
+
# NameSpace::Const == NameSpace::Const
|
271
|
+
# NameSpace::Const != ::Const
|
272
|
+
# Const != NameSpace::Const
|
273
|
+
def const_matches?(const, allowed_const)
|
274
|
+
parts = allowed_const.split('::').reverse.zip(
|
275
|
+
const.split('::').reverse
|
276
|
+
)
|
277
|
+
parts.all? do |(allowed_part, const_part)|
|
278
|
+
allowed_part == const_part.to_s
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def implicit_return?(node)
|
283
|
+
return false unless cop_config['AllowImplicitReturn']
|
284
|
+
|
285
|
+
node = assignable_node(node)
|
286
|
+
method, sibling_index = find_method_with_sibling_index(node.parent)
|
287
|
+
return unless method && (method.def_type? || method.block_type?)
|
288
|
+
|
289
|
+
method.children.size == node.sibling_index + sibling_index
|
290
|
+
end
|
291
|
+
|
292
|
+
def find_method_with_sibling_index(node, sibling_index = 1)
|
293
|
+
return node, sibling_index unless node&.or_type?
|
294
|
+
|
295
|
+
sibling_index += 1
|
296
|
+
|
297
|
+
find_method_with_sibling_index(node.parent, sibling_index)
|
298
|
+
end
|
299
|
+
|
300
|
+
def argument?(node)
|
301
|
+
assignable_node(node).argument?
|
302
|
+
end
|
303
|
+
|
304
|
+
def explicit_return?(node)
|
305
|
+
ret = assignable_node(node).parent
|
306
|
+
ret && (ret.return_type? || ret.next_type?)
|
307
|
+
end
|
308
|
+
|
309
|
+
def return_value_assigned?(node)
|
310
|
+
assignment = assignable_node(node).parent
|
311
|
+
assignment&.lvasgn_type?
|
312
|
+
end
|
313
|
+
|
314
|
+
def persist_method?(node, methods = PERSIST_METHODS)
|
315
|
+
methods.include?(node.method_name) &&
|
316
|
+
expected_signature?(node) &&
|
317
|
+
!allowed_receiver?(node)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Check argument signature as no arguments or one hash
|
321
|
+
def expected_signature?(node)
|
322
|
+
!node.arguments? ||
|
323
|
+
(node.arguments.one? &&
|
324
|
+
node.method_name != :destroy &&
|
325
|
+
(node.first_argument.hash_type? ||
|
326
|
+
!node.first_argument.literal?))
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|