Class: RuboCop::Cop::DevDoc::Rails::AvoidRailsCallbacks
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Rails::AvoidRailsCallbacks
- Defined in:
- lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb
Overview
Avoid Rails ActiveRecord callback DSL in model files.
Rationale
Rails callbacks (after_create, before_save, etc.) cause problems
with transaction ordering, hidden side effects, and unclear control
flow. When a callback fires is not always obvious to the reader, and
callbacks can trigger unexpectedly during testing or data migrations.
Instead, implement an explicit method that makes the side effect visible at the call site:
❌
class Order < ApplicationRecord
after_create :send_confirmation_email
end
✔️
class Order < ApplicationRecord
def save_with_confirmation_email
transaction do
saved = save
invoice.save! if saved
end
OrderMailer.confirmation(self).deliver_later if saved
end
end
The deliver_later is outside the transaction deliberately —
DevDoc/Rails/NoDeliverLaterInTransaction flags mailers/jobs
queued inside a transaction (the job may run before commit and
read stale data). The saved local leaks out of the block
(Ruby's normal scoping), gating the mailer on actual success.
When you have multiple call sites needing the same guard
The single-method-per-place pattern above works when there is one
natural call site. When several controllers/jobs/bulk operations all
need to enforce the same invariant before destroying or saving, the
temptation is to reach for before_destroy / before_save so
nothing slips through. There is a cleaner pattern that gives the
same guarantee without a callback — the fence-and-helper:
1. Alias the dangerous primitive as private, e.g.
alias _unguarded_hard_destroy hard_destroy
private :_unguarded_hard_destroy
2. Override the public name to raise with a pointer to the safe
method:
def hard_destroy
raise 'Use SomeModel#safely_hard_destroy — it keeps X in sync'
end
3. Expose a `safely_*` helper that does the guard, calls the
private alias, and runs any side effects, all wrapped in a
transaction so atomicity is a property of the helper rather
than of the caller:
def safely_hard_destroy
if invariant_would_be_violated?
errors.add(:base, '...')
return false
end
ApplicationRecord.transaction do
_unguarded_hard_destroy
cleanup_side_effects!
end
destroyed?
end
Direct calls to the dangerous primitive now raise with a helpful
message; the only legal path is the safe helper. Every existing
caller updates to safely_* and inherits the guard automatically.
Bonus property: ad-hoc bypass for ops/debugging is clean. Calling
model.send(:_unguarded_hard_destroy) in the Rails console is
scoped to the single call (no global state), self-documenting (you
have to type "unguarded"), and greppable. This is intended for
console / data-cleanup / debugging use only — if app code reaches
for send(:_unguarded_*), that's a design smell and should be
solved by extending the helper instead.
Inline disable
We have not yet found a case in our codebases where a callback was the right answer — explicit methods or the fence-and-helper pattern have always covered the legitimate intent. If you think you have a genuine exception, disable this cop inline with a written justification — expect pushback in review:
after_create :some_callback # rubocop:disable DevDoc/Rails/AvoidRailsCallbacks
# Reason: <explanation>
Both the symbol form and the block form are flagged:
❌ Symbol form
after_create :send_confirmation_email
❌ Block form
after_create { send_confirmation_email }
Constant Summary collapse
- CALLBACKS =
%i[ after_create after_create_commit after_save after_update after_destroy after_commit after_rollback after_initialize after_find after_touch before_create before_save before_update before_destroy before_validation after_validation around_create around_save around_update around_destroy ].freeze
- MSG =
'Avoid `%<method>s` — extract an explicit method (e.g. `save_with_*`) ' \ 'so the side effect is visible at the call site.'.freeze
- RESTRICT_ON_SEND =
CALLBACKS
Instance Method Summary collapse
-
#on_block(node) ⇒ Object
(also: #on_numblock)
rubocop:disable InternalAffairs/NumblockHandler.
- #on_send(node) ⇒ Object
Instance Method Details
#on_block(node) ⇒ Object Also known as: on_numblock
rubocop:disable InternalAffairs/NumblockHandler
134 135 136 137 138 139 |
# File 'lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb', line 134 def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler send_node = node.send_node return unless CALLBACKS.include?(send_node.method_name) add_offense(send_node.loc.selector, message: format(MSG, method: send_node.method_name)) end |
#on_send(node) ⇒ Object
130 131 132 |
# File 'lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb', line 130 def on_send(node) add_offense(node.loc.selector, message: format(MSG, method: node.method_name)) end |