Class: RuboCop::Cop::DevDoc::Rails::AvoidRailsCallbacks

Inherits:
Base
  • Object
show all
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
      save!
      OrderMailer.confirmation(self).deliver_later
    end
  end
end

## 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 }

Examples:

# bad
after_create :send_confirmation
before_save :normalize_name
around_update :wrap_in_audit_log

# bad (block form)
after_create { send_confirmation }

# good
def save_with_confirmation
  transaction { save! }
  send_confirmation
end

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

Instance Method Details

#on_block(node) ⇒ Object Also known as: on_numblock

rubocop:disable InternalAffairs/NumblockHandler



124
125
126
127
128
129
# File 'lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb', line 124

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



120
121
122
# File 'lib/rubocop/cop/dev_doc/rails/avoid_rails_callbacks.rb', line 120

def on_send(node)
  add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
end