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 ignore calls that return a boolean for success if the result is assigned to a variable or used as the condition in an if/unless statement. It will also ignore calls that return a model assigned to a variable that has a call to `persisted?`. Finally, it will ignore any call with more than 2 arguments as that is likely not an Active Record call or a Model.update(id, attributes) call.
@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
Constants
- CREATE_CONDITIONAL_MSG
- CREATE_MSG
- CREATE_PERSIST_METHODS
- MODIFY_PERSIST_METHODS
- MSG
- PERSIST_METHODS
Public Instance Methods
after_leaving_scope(scope, _variable_table)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 57 def after_leaving_scope(scope, _variable_table) scope.variables.each_value do |variable| variable.assignments.each do |assignment| check_assignment(assignment) end end end
autocorrect(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 92 def autocorrect(node) save_loc = node.loc.selector new_method = "#{node.method_name}!" ->(corrector) { corrector.replace(save_loc, new_method) } end
check_assignment(assignment)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 65 def check_assignment(assignment) node = right_assignment_node(assignment) return unless node return unless CREATE_PERSIST_METHODS.include?(node.method_name) return unless expected_signature?(node) return if persisted_referenced?(assignment) add_offense(node, location: :selector, message: format(CREATE_MSG, "#{node.method_name}!", node.method_name.to_s, node.method_name.to_s)) end
join_force?(force_class)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 53 def join_force?(force_class) force_class == VariableForce end
on_send(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 79 def on_send(node) return unless PERSIST_METHODS.include?(node.method_name) return unless expected_signature?(node) return if return_value_assigned?(node) return if check_used_in_conditional(node) return if last_call_of_method?(node) add_offense(node, location: :selector, message: format(MSG, "#{node.method_name}!", node.method_name.to_s)) end
Private Instance Methods
call_to_persisted?(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 115 def call_to_persisted?(node) node.send_type? && node.method?(:persisted?) end
check_used_in_conditional(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 119 def check_used_in_conditional(node) return false unless conditional?(node) unless MODIFY_PERSIST_METHODS.include?(node.method_name) add_offense(node, location: :selector, message: format(CREATE_CONDITIONAL_MSG, node.method_name.to_s)) end true end
conditional?(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 131 def conditional?(node) node.parent && ( node.parent.if_type? || node.parent.case_type? || node.parent.or_type? || node.parent.and_type? ) end
expected_signature?(node)
click to toggle source
Check argument signature as no arguments or one hash
# File lib/rubocop/cop/rails/save_bang.rb, line 151 def expected_signature?(node) !node.arguments? || (node.arguments.one? && node.method_name != :destroy && (node.first_argument.hash_type? || !node.first_argument.literal?)) end
last_call_of_method?(node)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 138 def last_call_of_method?(node) node.parent && node.parent.children.size == node.sibling_index + 1 end
persisted_referenced?(assignment)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 107 def persisted_referenced?(assignment) return unless assignment.referenced? assignment.variable.references.any? do |reference| call_to_persisted?(reference.node.parent) end end
return_value_assigned?(node)
click to toggle source
Ignore simple assignment or if condition
# File lib/rubocop/cop/rails/save_bang.rb, line 143 def return_value_assigned?(node) return false unless node.parent node.parent.lvasgn_type? || (node.parent.block_type? && node.parent.parent && node.parent.parent.lvasgn_type?) end
right_assignment_node(assignment)
click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 101 def right_assignment_node(assignment) node = assignment.node.child_nodes.first return node unless node && node.block_type? node.child_nodes.first end