class RuboCop::Cop::Rails::SaveBang

end
. . .
unless user.persisted?
user = User.find_or_create_by(name: ‘Joe’)
user.destroy!
user.find_or_create_by!(name: ‘Joe’)
user.update!(name: ‘Joe’)
user.save!
end
. . .
unless user.save
# good
user.destroy
user.find_or_create_by(name: ‘Joe’)
user.update(name: ‘Joe’)
user.save
# bad
@example
call or a Model.update(id, attributes) call.
call with more than 2 arguments as that is likely not an Active Record
variable that has a call to ‘persisted?`. Finally, it will ignore any
statement. It will also ignore calls that return a model assigned to a
is assigned to a variable or used as the condition in an if/unless
This will ignore calls that return a boolean for success if the result
save and an exception is better than unhandled failure.
should be used instead of save because the model might have failed to
This cop identifies possible cases where Active Record save! or related

def after_leaving_scope(scope, _variable_table)

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 autocorrect(node)

def autocorrect(node)
  save_loc = node.loc.selector
  new_method = "#{node.method_name}!"
  ->(corrector) { corrector.replace(save_loc, new_method) }
end

def call_to_persisted?(node)

def call_to_persisted?(node)
  node.send_type? && node.method?(:persisted?)
end

def check_assignment(assignment)

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, :selector,
              format(CREATE_MSG,
                     "#{node.method_name}!",
                     node.method_name.to_s,
                     node.method_name.to_s))
end

def check_used_in_conditional(node)

def check_used_in_conditional(node)
  return false unless conditional?(node)
  unless MODIFY_PERSIST_METHODS.include?(node.method_name)
    add_offense(node, :selector,
                format(CREATE_CONDITIONAL_MSG,
                       node.method_name.to_s))
  end
  true
end

def conditional?(node)

def conditional?(node)
  node.parent && (
    node.parent.if_type? || node.parent.case_type? ||
    node.parent.or_type? || node.parent.and_type?
  )
end

def expected_signature?(node)

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

def join_force?(force_class)

def join_force?(force_class)
  force_class == VariableForce
end

def last_call_of_method?(node)

def last_call_of_method?(node)
  node.parent && node.parent.children.size == node.sibling_index + 1
end

def on_send(node)

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, :selector,
              format(MSG,
                     "#{node.method_name}!",
                     node.method_name.to_s))
end

def persisted_referenced?(assignment)

def persisted_referenced?(assignment)
  return unless assignment.referenced?
  assignment.variable.references.any? do |reference|
    call_to_persisted?(reference.node.parent)
  end
end

def return_value_assigned?(node)

Ignore simple assignment or if condition
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

def right_assignment_node(assignment)

def right_assignment_node(assignment)
  node = assignment.node.child_nodes.first
  return node unless node && node.block_type?
  node.child_nodes.first
end