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