Class: RuboCop::Cop::Rails::SaveBang

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
NegativeConditional
Defined in:
lib/rubocop/cop/rails/save_bang.rb

Overview

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:

  • update or save calls, assigned to a variable, or used as a condition in an if/unless/case statement.

  • create calls, assigned to a variable that then has a call to `persisted?`, or whose return value is checked by `persisted?` immediately

  • calls if the result is explicitly returned from methods and blocks, or provided as arguments.

  • calls whose signature doesn't look like an ActiveRecord persistence method.

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: []`

Examples:


# 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

AllowImplicitReturn: true (default)


# good
users.each { |u| u.save }

def save_user
  user.save
end

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

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

Constant Summary collapse

MSG =
'Use `%<prefer>s` instead of `%<current>s` if the return ' \
'value is not checked.'
CREATE_MSG =
(MSG +
' Or check `persisted?` on model returned from ' \
'`%<current>s`.').freeze
CREATE_CONDITIONAL_MSG =
'`%<current>s` returns a model which is ' \
'always truthy.'
CREATE_PERSIST_METHODS =
%i[create create_or_find_by
first_or_create find_or_create_by].freeze
MODIFY_PERSIST_METHODS =
%i[save
update update_attributes destroy].freeze
RESTRICT_ON_SEND =
(CREATE_PERSIST_METHODS + MODIFY_PERSIST_METHODS).freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.joining_forcesObject



138
139
140
# File 'lib/rubocop/cop/rails/save_bang.rb', line 138

def self.joining_forces
  VariableForce
end

Instance Method Details

#after_leaving_scope(scope, _variable_table) ⇒ Object



142
143
144
145
146
147
148
# File 'lib/rubocop/cop/rails/save_bang.rb', line 142

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) ⇒ Object



150
151
152
153
154
155
156
157
158
# File 'lib/rubocop/cop/rails/save_bang.rb', line 150

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_send(node) ⇒ Object Also known as: on_csend

rubocop:disable Metrics/CyclomaticComplexity



161
162
163
164
165
166
167
168
169
170
171
# File 'lib/rubocop/cop/rails/save_bang.rb', line 161

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