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
RESTRICT_ON_SEND

Public Class Methods

joining_forces() click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 119
def self.joining_forces
  VariableForce
end

Public Instance Methods

after_leaving_scope(scope, _variable_table) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 123
def after_leaving_scope(scope, _variable_table)
  scope.variables.each_value do |variable|
    variable.assignments.each do |assignment|
      check_assignment(assignment)
    end
  end
end
check_assignment(assignment) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 131
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)

  register_offense(node, CREATE_MSG)
end
on_csend(node)

rubocop:enable Metrics/CyclomaticComplexity

Alias for: on_send
on_send(node) click to toggle source

rubocop:disable Metrics/CyclomaticComplexity

# File lib/rubocop/cop/rails/save_bang.rb, line 142
def on_send(node)
  return unless persist_method?(node)
  return if return_value_assigned?(node)
  return if implicit_return?(node)
  return if check_used_in_condition_or_compound_boolean(node)
  return if argument?(node)
  return if explicit_return?(node)
  return if checked_immediately?(node)

  register_offense(node, MSG)
end
Also aliased as: on_csend

Private Instance Methods

allowed_receiver?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 244
def allowed_receiver?(node)
  return false unless node.receiver
  return true if node.receiver.const_name == 'ENV'
  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 304
def argument?(node)
  assignable_node(node).argument?
end
array_parent(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 208
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 189
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 185
def call_to_persisted?(node)
  node.send_type? && node.method?(:persisted?)
end
check_used_in_condition_or_compound_boolean(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 215
def check_used_in_condition_or_compound_boolean(node)
  return false unless in_condition_or_compound_boolean?(node)

  register_offense(node, CREATE_CONDITIONAL_MSG) unless MODIFY_PERSIST_METHODS.include?(node.method_name)

  true
end
checked_immediately?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 240
def checked_immediately?(node)
  node.parent && call_to_persisted?(node.parent)
end
conditional?(parent) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 236
def conditional?(parent)
  parent.if_type? || parent.case_type?
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 277
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 325
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 308
def explicit_return?(node)
  ret = assignable_node(node).parent
  ret && (ret.return_type? || ret.next_type?)
end
find_method_with_sibling_index(node, sibling_index = 1) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 296
def find_method_with_sibling_index(node, sibling_index = 1)
  return node, sibling_index unless node&.or_type?

  sibling_index += 1

  find_method_with_sibling_index(node.parent, sibling_index)
end
hash_parent(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 198
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 286
def implicit_return?(node)
  return false unless cop_config['AllowImplicitReturn']

  node = assignable_node(node)
  method, sibling_index = find_method_with_sibling_index(node.parent)
  return unless method && (method.def_type? || method.block_type?)

  method.children.size == node.sibling_index + sibling_index
end
in_condition_or_compound_boolean?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 223
def in_condition_or_compound_boolean?(node)
  node = node.block_node || node
  parent = node.parent
  return false unless parent

  operator_or_single_negative?(parent) ||
    (conditional?(parent) && node == parent.condition)
end
operator_or_single_negative?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 232
def operator_or_single_negative?(node)
  node.or_type? || node.and_type? || single_negative?(node)
end
persist_method?(node, methods = RESTRICT_ON_SEND) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 318
def persist_method?(node, methods = RESTRICT_ON_SEND)
  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 177
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 254
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?(receiver_part.to_sym)
    elsif node.const_type?
      const_matches?(node.const_name, receiver_part)
    end
  end
end
register_offense(node, msg) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 158
def register_offense(node, msg)
  current_method = node.method_name
  bang_method = "#{current_method}!"
  full_message = format(msg, prefer: bang_method, current: current_method)

  range = node.loc.selector
  add_offense(range, message: full_message) do |corrector|
    corrector.replace(range, bang_method)
  end
end
return_value_assigned?(node) click to toggle source
# File lib/rubocop/cop/rails/save_bang.rb, line 313
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 169
def right_assignment_node(assignment)
  node = assignment.node.child_nodes.first

  return node unless node&.block_type?

  node.send_node
end