class RuboCop::Cop::Rails::SaveBang
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?`, 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: []`
@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
Constants
- CREATE_CONDITIONAL_MSG
- CREATE_MSG
- CREATE_PERSIST_METHODS
- MODIFY_PERSIST_METHODS
- MSG
- RESTRICT_ON_SEND
Public Class Methods
# File lib/rubocop/cop/rails/save_bang.rb, line 119 def self.joining_forces VariableForce end
Public Instance Methods
# File lib/rubocop/cop/rails/save_bang.rb, line 123 def after_leaving_scope(scope, _variable_table) scope.variables.each_value do |variable| variable.assignments.each do |assignment| check_assignment(assignment) end end end
# File lib/rubocop/cop/rails/save_bang.rb, line 131 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
# File lib/rubocop/cop/rails/save_bang.rb, line 142 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
Private Instance Methods
# File lib/rubocop/cop/rails/save_bang.rb, line 244 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
# File lib/rubocop/cop/rails/save_bang.rb, line 304 def argument?(node) assignable_node(node).argument? end
# File lib/rubocop/cop/rails/save_bang.rb, line 208 def array_parent(node) array = node.parent return unless array&.array_type? array end
# File lib/rubocop/cop/rails/save_bang.rb, line 189 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
# File lib/rubocop/cop/rails/save_bang.rb, line 185 def call_to_persisted?(node) node.send_type? && node.method?(:persisted?) end
# File lib/rubocop/cop/rails/save_bang.rb, line 215 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
# File lib/rubocop/cop/rails/save_bang.rb, line 240 def checked_immediately?(node) node.parent && call_to_persisted?(node.parent) end
# File lib/rubocop/cop/rails/save_bang.rb, line 236 def conditional?(parent) parent.if_type? || parent.case_type? end
Const == Const ::Const == ::Const ::Const == Const Const == ::Const NameSpace::Const == Const NameSpace::Const == NameSpace::Const NameSpace::Const != ::Const Const != NameSpace::Const
# File lib/rubocop/cop/rails/save_bang.rb, line 277 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
Check argument signature as no arguments or one hash
# File lib/rubocop/cop/rails/save_bang.rb, line 325 def expected_signature?(node) !node.arguments? || (node.arguments.one? && node.method_name != :destroy && (node.first_argument.hash_type? || !node.first_argument.literal?)) end
# File lib/rubocop/cop/rails/save_bang.rb, line 308 def explicit_return?(node) ret = assignable_node(node).parent ret && (ret.return_type? || ret.next_type?) end
# File lib/rubocop/cop/rails/save_bang.rb, line 296 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
# File lib/rubocop/cop/rails/save_bang.rb, line 198 def hash_parent(node) pair = node.parent return unless pair&.pair_type? hash = pair.parent return unless hash&.hash_type? hash end
# File lib/rubocop/cop/rails/save_bang.rb, line 286 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
# File lib/rubocop/cop/rails/save_bang.rb, line 223 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
# File lib/rubocop/cop/rails/save_bang.rb, line 232 def operator_or_single_negative?(node) node.or_type? || node.and_type? || single_negative?(node) end
# File lib/rubocop/cop/rails/save_bang.rb, line 318 def persist_method?(node, methods = RESTRICT_ON_SEND) methods.include?(node.method_name) && expected_signature?(node) && !allowed_receiver?(node) end
# File lib/rubocop/cop/rails/save_bang.rb, line 177 def persisted_referenced?(assignment) return unless assignment.referenced? assignment.variable.references.any? do |reference| call_to_persisted?(reference.node.parent) end end
# File lib/rubocop/cop/rails/save_bang.rb, line 254 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
# File lib/rubocop/cop/rails/save_bang.rb, line 158 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
# File lib/rubocop/cop/rails/save_bang.rb, line 313 def return_value_assigned?(node) assignment = assignable_node(node).parent assignment&.lvasgn_type? end
# File lib/rubocop/cop/rails/save_bang.rb, line 169 def right_assignment_node(assignment) node = assignment.node.child_nodes.first return node unless node&.block_type? node.send_node end