Class: RuboCop::Cop::DevDoc::Rails::NoManualRecordInvalid
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Rails::NoManualRecordInvalid
- Defined in:
- lib/rubocop/cop/dev_doc/rails/no_manual_record_invalid.rb
Overview
Flag hand-raised (or hand-constructed) ActiveRecord::RecordInvalid
in model code. Only Active Record's own bang persistence
(save!/create!/update!) should raise it.
Rationale
RecordInvalid is the exception Active Record raises when a bang
persistence call fails validation. Raising it by hand to signal a
domain precondition ("only drafts can be approved") borrows the
framework's exception to smuggle soft-failure data (the errors on
the record) through a raise — and the caller then needs a rescue
plus a comment explaining that the errors are already on the model.
The soft shape says the same thing without the detour: add the
error, return false, and let the caller check the result and render
record.errors exactly as it does for a plain failed save.
❌ hand-raised to reuse the rescue/rendering machinery
def approve!
errors.add(:base, 'Only drafts can be approved') unless draft?
raise ActiveRecord::RecordInvalid.new(self) if errors.any?
update!(approved_at: Time.current)
end
✔️ soft failure — the caller checks the return and renders errors
def approve
unless draft?
errors.add(:base, 'Only drafts can be approved')
return false
end
self.approved_at = Time.current
save
end
Relationship with DevDoc/Rails/SoftFailureInBangMethod
The two cops close the two spellings of the same confusion: that one catches soft failure hiding under a raising name; this one catches a raise simulating a validation failure. Together they funnel controller-invoked domain actions to the soft non-bang shape.
Exception
Re-raising a rescued RecordInvalid (raise e, or a bare raise
inside the rescue) is not flagged — the exception originated in
Active Record, not by hand. A genuine need to construct one (for
example, aborting a batch import through machinery that renders
RecordInvalid specifically) takes an inline disable stating the
reason.
NOTE: Indirection is not detected — the class stashed in a variable
before raising, or an app-defined RecordInvalid subclass —
reviewers must cover those.
Constant Summary collapse
- MSG =
'Do not raise `ActiveRecord::RecordInvalid` by hand — only Active Record persistence ' \ '(`save!`, `create!`) should raise it. Signal domain failure softly: add to `errors` ' \ 'and return false.'.freeze
- RESTRICT_ON_SEND =
%i[new raise fail].freeze
Instance Method Summary collapse
Instance Method Details
#on_send(node) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/rubocop/cop/dev_doc/rails/no_manual_record_invalid.rb', line 66 def on_send(node) if node.method_name == :new return unless record_invalid_const?(node.receiver) else return unless node.receiver.nil? # `raise ActiveRecord::RecordInvalid.new(...)` is already flagged # at the `.new` itself; this branch covers the class-only form # `raise ActiveRecord::RecordInvalid`. argument = node.first_argument return unless argument && record_invalid_const?(argument) end add_offense(node) end |