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:

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

after_leaving_scope(scope, _variable_table) click to toggle source
# 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
autocorrect(node) click to toggle source
# 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
check_assignment(assignment) click to toggle source
# 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
join_force?(force_class) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 117
def join_force?(force_class)
  force_class == VariableForce
end
on_csend(node)
Alias for: on_send
on_send(node) click to toggle source
# 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
Also aliased as: on_csend

Private Instance Methods

add_offense_for_node(node, msg = MSG) click to toggle source
# 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
allowed_receiver?(node) click to toggle source
# 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
argument?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 285
def argument?(node)
  assignable_node(node).argument?
end
array_parent(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 206
def array_parent(node)
  array = node.parent
  return unless array&.array_type?

  array
end
assignable_node(node) click to toggle source
# 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
call_to_persisted?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 183
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 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
conditional?(node) click to toggle source
# 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_matches?(const, allowed_const) click to toggle source

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
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 306
def expected_signature?(node)
  !node.arguments? ||
    (node.arguments.one? &&
      node.method_name != :destroy &&
      (node.first_argument.hash_type? ||
      !node.first_argument.literal?))
end
explicit_return?(node) click to toggle source
# 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
hash_parent(node) click to toggle source
# 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
implicit_return?(node) click to toggle source
# 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
persist_method?(node, methods = PERSIST_METHODS) click to toggle source
# 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
persisted_referenced?(assignment) click to toggle source
# 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
receiver_chain_matches?(node, allowed_receiver) click to toggle source
# 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
return_value_assigned?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 294
def return_value_assigned?(node)
  assignment = assignable_node(node).parent
  assignment&.lvasgn_type?
end
right_assignment_node(assignment) click to toggle source
# 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