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