forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsave_bang.rb
342 lines (293 loc) · 10.2 KB
/
save_bang.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# Identifies possible cases where Active Record save! or related
# should be used instead of save because the model might have failed to
# save and an exception is better than unhandled failure.
#
# This will allow:
#
# * update or save calls, assigned to a variable,
# or used as a condition in an if/unless/case statement.
# * create calls, assigned to a variable that then has a
# call to `persisted?`, or whose return value is checked by
# `persisted?` immediately
# * calls if the result is explicitly returned from methods and blocks,
# or provided as arguments.
# * calls whose signature doesn't look like an ActiveRecord
# persistence method.
#
# By default it will also allow implicit returns from methods and blocks.
# that behavior can be turned off with `AllowImplicitReturn: false`.
#
# You can permit receivers that are giving false positives with
# `AllowedReceivers: []`
#
# @safety
# This cop's autocorrection is unsafe because a custom `update` method call would be changed to `update!`,
# but the method name in the definition would be unchanged.
#
# [source,ruby]
# ----
# # Original code
# def update_attributes
# end
#
# update_attributes
#
# # After running rubocop --safe-autocorrect
# def update_attributes
# end
#
# update
# ----
#
# @example
#
# # bad
# user.save
# user.update(name: 'Joe')
# user.find_or_create_by(name: 'Joe')
# user.destroy
#
# # good
# unless user.save
# # ...
# end
# user.save!
# user.update!(name: 'Joe')
# user.find_or_create_by!(name: 'Joe')
# user.destroy!
#
# user = User.find_or_create_by(name: 'Joe')
# unless user.persisted?
# # ...
# end
#
# def save_user
# return user.save
# end
#
# @example AllowImplicitReturn: true (default)
#
# # good
# users.each { |u| u.save }
#
# def save_user
# user.save
# end
#
# @example AllowImplicitReturn: false
#
# # bad
# users.each { |u| u.save }
# def save_user
# user.save
# end
#
# # good
# users.each { |u| u.save! }
#
# def save_user
# user.save!
# end
#
# def save_user
# return user.save
# end
#
# @example AllowedReceivers: ['merchant.customers', 'Service::Mailer']
#
# # bad
# merchant.create
# customers.builder.save
# Mailer.create
#
# module Service::Mailer
# self.create
# end
#
# # good
# merchant.customers.create
# MerchantService.merchant.customers.destroy
# Service::Mailer.update(message: 'Message')
# ::Service::Mailer.update
# Services::Service::Mailer.update(message: 'Message')
# Service::Mailer::update
#
class SaveBang < Base
include NegativeConditional
extend AutoCorrector
MSG = 'Use `%<prefer>s` instead of `%<current>s` if the return value is not checked.'
CREATE_MSG = "#{MSG} Or check `persisted?` on model returned from `%<current>s`."
CREATE_CONDITIONAL_MSG = '`%<current>s` returns a model which is always truthy.'
CREATE_PERSIST_METHODS = %i[create create_or_find_by first_or_create find_or_create_by].freeze
MODIFY_PERSIST_METHODS = %i[save update update_attributes destroy].freeze
RESTRICT_ON_SEND = (CREATE_PERSIST_METHODS + MODIFY_PERSIST_METHODS).freeze
def self.joining_forces
VariableForce
end
def after_leaving_scope(scope, _variable_table)
scope.variables.each_value do |variable|
variable.assignments.each do |assignment|
check_assignment(assignment)
end
end
end
def check_assignment(assignment)
node = right_assignment_node(assignment)
return unless node&.send_type?
return unless persist_method?(node, CREATE_PERSIST_METHODS)
return if persisted_referenced?(assignment)
register_offense(node, CREATE_MSG)
end
# rubocop:disable Metrics/CyclomaticComplexity
def on_send(node)
return unless persist_method?(node)
return if return_value_assigned?(node)
return if implicit_return?(node)
return if check_used_in_condition_or_compound_boolean(node)
return if argument?(node)
return if explicit_return?(node)
return if checked_immediately?(node)
register_offense(node, MSG)
end
# rubocop:enable Metrics/CyclomaticComplexity
alias on_csend on_send
private
def register_offense(node, msg)
current_method = node.method_name
bang_method = "#{current_method}!"
full_message = format(msg, prefer: bang_method, current: current_method)
range = node.loc.selector
add_offense(range, message: full_message) do |corrector|
corrector.replace(range, bang_method)
end
end
def right_assignment_node(assignment)
node = assignment.node.child_nodes.first
return node unless node&.block_type?
node.send_node
end
def persisted_referenced?(assignment)
return unless assignment.referenced?
assignment.variable.references.any? do |reference|
call_to_persisted?(reference.node.parent)
end
end
def call_to_persisted?(node)
node.send_type? && node.method?(:persisted?)
end
def assignable_node(node)
assignable = node.block_node || node
while node
node = hash_parent(node) || array_parent(node)
assignable = node if node
end
assignable
end
def hash_parent(node)
pair = node.parent
return unless pair&.pair_type?
hash = pair.parent
return unless hash&.hash_type?
hash
end
def array_parent(node)
array = node.parent
return unless array&.array_type?
array
end
def check_used_in_condition_or_compound_boolean(node)
return false unless in_condition_or_compound_boolean?(node)
register_offense(node, CREATE_CONDITIONAL_MSG) unless MODIFY_PERSIST_METHODS.include?(node.method_name)
true
end
def in_condition_or_compound_boolean?(node)
node = node.block_node || node
parent = node.parent
return false unless parent
operator_or_single_negative?(parent) || (conditional?(parent) && node == parent.condition)
end
def operator_or_single_negative?(node)
node.or_type? || node.and_type? || single_negative?(node)
end
def conditional?(parent)
parent.if_type? || parent.case_type?
end
def checked_immediately?(node)
node.parent && call_to_persisted?(node.parent)
end
def allowed_receiver?(node)
return false unless node.receiver
return true if node.receiver.const_name == 'ENV'
return false unless cop_config['AllowedReceivers']
cop_config['AllowedReceivers'].any? do |allowed_receiver|
receiver_chain_matches?(node, allowed_receiver)
end
end
def receiver_chain_matches?(node, allowed_receiver)
allowed_receiver.split('.').reverse.all? do |receiver_part|
node = node.receiver
return false unless node
if node.variable?
node.node_parts.first == receiver_part.to_sym
elsif node.send_type?
node.method?(receiver_part.to_sym)
elsif node.const_type?
const_matches?(node.const_name, receiver_part)
end
end
end
# Const == Const
# ::Const == ::Const
# ::Const == Const
# Const == ::Const
# NameSpace::Const == Const
# NameSpace::Const == NameSpace::Const
# NameSpace::Const != ::Const
# Const != NameSpace::Const
def const_matches?(const, allowed_const)
parts = allowed_const.split('::').reverse.zip(const.split('::').reverse)
parts.all? do |(allowed_part, const_part)|
allowed_part == const_part.to_s
end
end
def implicit_return?(node)
return false unless cop_config['AllowImplicitReturn']
node = assignable_node(node)
method, sibling_index = find_method_with_sibling_index(node.parent)
return unless method && (method.def_type? || method.block_type?)
method.children.size == node.sibling_index + sibling_index
end
def find_method_with_sibling_index(node, sibling_index = 1)
return node, sibling_index unless node&.or_type?
sibling_index += 1
find_method_with_sibling_index(node.parent, sibling_index)
end
def argument?(node)
assignable_node(node).argument?
end
def explicit_return?(node)
ret = assignable_node(node).parent
ret && (ret.return_type? || ret.next_type?)
end
def return_value_assigned?(node)
assignment = assignable_node(node).parent
assignment&.lvasgn_type?
end
def persist_method?(node, methods = RESTRICT_ON_SEND)
methods.include?(node.method_name) && expected_signature?(node) && !allowed_receiver?(node)
end
# Check argument signature as no arguments or one hash
def expected_signature?(node)
!node.arguments? ||
(node.arguments.one? &&
node.method_name != :destroy &&
(node.first_argument.hash_type? || !node.first_argument.literal?))
end
end
end
end
end