class RuboCop::Cop::Rails::SaveBang
Service::Mailer::update
Services::Service::Mailer.update(message: ‘Message’)
::Service::Mailer.update
Service::Mailer.update(message: ‘Message’)
MerchantService.merchant.customers.destroy
merchant.customers.create
# good
end
self.create
module Service::Mailer
Mailer.create
customers.builder.save
merchant.create
# bad
@example AllowedReceivers: [‘merchant.customers’, ‘Service::Mailer’]
end
return user.save
def save_user
end
user.save!
def save_user
users.each { |u| u.save! }
# good
end
user.save
def save_user
users.each { |u| u.save }
# bad
@example AllowImplicitReturn: false
end
user.save
def save_user
users.each { |u| u.save }
# good
@example AllowImplicitReturn: true (default)
end
return user.save
def save_user
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
—-
update
end
def update_attributes
# After running rubocop –safe-autocorrect
update_attributes
end
def update_attributes
# Original code
—-
[source,ruby]
but the method name in the definition would be unchanged.
This cop’s autocorrection is unsafe because a custom ‘update` method call would be changed to `update!`,
@safety
`AllowedReceivers: []`
You can permit receivers that are giving false positives with
that behavior can be turned off with `AllowImplicitReturn: false`.
By default it will also allow implicit returns from methods and blocks.
persistence method.
* calls whose signature doesn’t look like an ActiveRecord
or provided as arguments.
* calls if the result is explicitly returned from methods and blocks,
‘persisted?` immediately
call to `persisted?`, or whose return value is checked by
* create calls, assigned to a variable that then has a
or used as a condition in an if/unless/case statement.
* update or save calls, assigned to a variable,
This will allow:
save and an exception is better than unhandled failure.
should be used instead of save because the model might have failed to
Identifies possible cases where Active Record save! or related
def self.joining_forces
def self.joining_forces VariableForce end
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 allowed_receiver?(node)
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
def argument?(node)
def argument?(node) assignable_node(node).argument? end
def array_parent(node)
def array_parent(node) array = node.parent return unless array&.array_type? array end
def assignable_node(node)
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
def call_to_persisted?(node)
def call_to_persisted?(node) node = node.parent.condition if node.parenthesized_call? && node.parent.if_type? node.send_type? && node.method?(:persisted?) end
def check_assignment(assignment)
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
def check_used_in_condition_or_compound_boolean(node)
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
def checked_immediately?(node)
def checked_immediately?(node) node.parent && call_to_persisted?(node.parent) end
def conditional?(parent)
def conditional?(parent) parent.type?(:if, :case) end
def const_matches?(const, allowed_const)
NameSpace::Const != ::Const
NameSpace::Const == NameSpace::Const
NameSpace::Const == Const
Const == ::Const
::Const == Const
::Const == ::Const
Const == Const
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
def deparenthesize(node)
def deparenthesize(node) node = node.children.last while node.begin_type? node end
def expected_signature?(node)
def expected_signature?(node) return true unless node.arguments? return false if !node.arguments.one? || node.method?(:destroy) node.first_argument.hash_type? || !node.first_argument.literal? end
def explicit_return?(node)
def explicit_return?(node) ret = assignable_node(node).parent ret&.type?(:return, :next) end
def find_method_with_sibling_index(node, sibling_index = 1)
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
def hash_parent(node)
def hash_parent(node) pair = node.parent return unless pair&.pair_type? hash = pair.parent return unless hash&.hash_type? hash end
def implicit_return?(node)
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 false unless method&.type?(:def, :any_block) method.children.size == node.sibling_index + sibling_index end
def in_condition_or_compound_boolean?(node)
def in_condition_or_compound_boolean?(node) node = node.block_node || node parent = node.each_ancestor.find { |ancestor| !ancestor.begin_type? } return false unless parent operator_or_single_negative?(parent) || (conditional?(parent) && node == deparenthesize(parent.condition)) end
def on_send(node)
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
def operator_or_single_negative?(node)
def operator_or_single_negative?(node) node.operator_keyword? || single_negative?(node) end
def persist_method?(node, methods = RESTRICT_ON_SEND)
def persist_method?(node, methods = RESTRICT_ON_SEND) methods.include?(node.method_name) && expected_signature?(node) && !allowed_receiver?(node) end
def persisted_referenced?(assignment)
def persisted_referenced?(assignment) return false unless assignment.referenced? assignment.variable.references.any? do |reference| call_to_persisted?(reference.node.parent) end end
def receiver_chain_matches?(node, allowed_receiver)
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
def register_offense(node, msg)
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
def return_value_assigned?(node)
def return_value_assigned?(node) return false unless (assignment = assignable_node(node).parent) assignment.assignment? end
def right_assignment_node(assignment)
def right_assignment_node(assignment) node = assignment.node.child_nodes.first return node unless node&.any_block_type? node.send_node end