Class: RuboCop::Cop::DevDoc::Rails::NoManualRecordInvalid

Inherits:
Base
  • Object
show all
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