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?`.
-
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
- PERSIST_METHODS
Public Instance Methods
# File lib/rubocop/cop/rails/save_bang.rb, line 121 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 151 def autocorrect(node) save_loc = node.loc.selector new_method = "#{node.method_name}!" ->(corrector) { corrector.replace(save_loc, new_method) } end
# File lib/rubocop/cop/rails/save_bang.rb, line 129 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
# File lib/rubocop/cop/rails/save_bang.rb, line 117 def join_force?(force_class) force_class == VariableForce end
# File lib/rubocop/cop/rails/save_bang.rb, line 139 def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity return unless persist_method?(node) return if return_value_assigned?(node) return if check_used_in_conditional(node) return if argument?(node) return if implicit_return?(node) return if explicit_return?(node) add_offense_for_node(node) end
Private Instance Methods
# File lib/rubocop/cop/rails/save_bang.rb, line 160 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
# File lib/rubocop/cop/rails/save_bang.rb, line 234 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
# File lib/rubocop/cop/rails/save_bang.rb, line 285 def argument?(node) assignable_node(node).argument? end
# File lib/rubocop/cop/rails/save_bang.rb, line 206 def array_parent(node) array = node.parent return unless array&.array_type? array end
# File lib/rubocop/cop/rails/save_bang.rb, line 187 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 183 def call_to_persisted?(node) node.send_type? && node.method?(:persisted?) end
# File lib/rubocop/cop/rails/save_bang.rb, line 213 def check_used_in_conditional(node) return false unless conditional?(node) unless MODIFY_PERSIST_METHODS.include?(node.method_name) add_offense_for_node(node, CREATE_CONDITIONAL_MSG) end true end
# File lib/rubocop/cop/rails/save_bang.rb, line 223 def conditional?(node) # rubocop:disable Metrics/CyclomaticComplexity node = node.block_node || node condition = node.parent return false unless condition condition.if_type? || condition.case_type? || condition.or_type? || condition.and_type? || single_negative?(condition) 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 266 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 306 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 289 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 196 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 275 def implicit_return?(node) return false unless cop_config['AllowImplicitReturn'] node = assignable_node(node) method = node.parent return unless method && (method.def_type? || method.block_type?) method.children.size == node.sibling_index + 1 end
# File lib/rubocop/cop/rails/save_bang.rb, line 299 def persist_method?(node, methods = PERSIST_METHODS) methods.include?(node.method_name) && expected_signature?(node) && !allowed_receiver?(node) end
# File lib/rubocop/cop/rails/save_bang.rb, line 175 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 243 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_name == 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 294 def return_value_assigned?(node) assignment = assignable_node(node).parent assignment&.lvasgn_type? end
# File lib/rubocop/cop/rails/save_bang.rb, line 167 def right_assignment_node(assignment) node = assignment.node.child_nodes.first return node unless node&.block_type? node.send_node end