forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsave_bang.rb
331 lines (281 loc) · 9.73 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
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# This cop 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?`.
# - 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: []`
#
# @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 < Cop
include NegativeConditional
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`.').freeze
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
PERSIST_METHODS = (CREATE_PERSIST_METHODS +
MODIFY_PERSIST_METHODS).freeze
def join_force?(force_class)
force_class == 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)
add_offense_for_node(node, CREATE_MSG)
end
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
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)
add_offense_for_node(node)
end
alias on_csend on_send
def autocorrect(node)
save_loc = node.loc.selector
new_method = "#{node.method_name}!"
->(corrector) { corrector.replace(save_loc, new_method) }
end
private
def add_offense_for_node(node, msg = MSG)
name = node.method_name
full_message = format(msg, prefer: "#{name}!", current: name.to_s)
add_offense(node, location: :selector, message: full_message)
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)
unless MODIFY_PERSIST_METHODS.include?(node.method_name)
add_offense_for_node(node, CREATE_CONDITIONAL_MSG)
end
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 allowed_receiver?(node)
return false unless node.receiver
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 = PERSIST_METHODS)
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