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

Inherits:
Cop
  • Object
show all
Includes:
NegativeConditional
Defined in:
lib/rubocop/cop/rails/save_bang.rb

Overview

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:

  • 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?`.

  • 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.'.freeze
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.'.freeze
CREATE_PERSIST_METHODS =
%i[create
first_or_create find_or_create_by].freeze
MODIFY_PERSIST_METHODS =
%i[save
update update_attributes destroy].freeze
PERSIST_METHODS =
(CREATE_PERSIST_METHODS +
MODIFY_PERSIST_METHODS).freeze

Constants included from Util

Util::LITERAL_REGEX

Instance Attribute Summary

Attributes inherited from Cop

#config, #corrections, #offenses, #processed_source

Instance Method Summary collapse

Methods included from NodePattern::Macros

#def_node_matcher, #def_node_search, #node_search, #node_search_all, #node_search_body, #node_search_first

Methods inherited from Cop

#add_offense, all, autocorrect_incompatible_with, badge, #config_to_allow_offenses, #config_to_allow_offenses=, #cop_config, cop_name, #cop_name, #correct, department, #duplicate_location?, #excluded_file?, #find_location, #highlights, inherited, #initialize, lint?, match?, #message, #messages, non_rails, #parse, qualified_cop_name, #relevant_file?, #target_rails_version, #target_ruby_version

Methods included from AST::Sexp

#s

Methods included from AutocorrectLogic

#autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #support_autocorrect?

Methods included from IgnoredNode

#ignore_node, #ignored_node?, #part_of_ignored_node?

Methods included from Util

begins_its_line?, comment_line?, double_quotes_required?, escape_string, first_part_of_call_chain, interpret_string_escapes, line_range, needs_escaping?, on_node, parentheses?, same_line?, to_string_literal, to_supported_styles, tokens

Methods included from PathUtil

absolute?, hidden_dir?, hidden_file_in_not_hidden_dir?, match_path?, pwd, relative_path, reset_pwd, smart_path

Constructor Details

This class inherits a constructor from RuboCop::Cop::Cop

Instance Method Details

#after_leaving_scope(scope, _variable_table) ⇒ Object



121
122
123
124
125
126
127
# 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) ⇒ Object



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

def autocorrect(node)
  save_loc = node.loc.selector
  new_method = "#{node.method_name}!"

  ->(corrector) { corrector.replace(save_loc, new_method) }
end

#check_assignment(assignment) ⇒ Object



129
130
131
132
133
134
135
136
137
# File 'lib/rubocop/cop/rails/save_bang.rb', line 129

def check_assignment(assignment)
  node = right_assignment_node(assignment)

  return unless node && 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) ⇒ Boolean



117
118
119
# File 'lib/rubocop/cop/rails/save_bang.rb', line 117

def join_force?(force_class)
  force_class == VariableForce
end

#on_send(node) ⇒ Object

rubocop:disable Metrics/CyclomaticComplexity



139
140
141
142
143
144
145
146
147
148
# 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