Class: RuboCop::Cop::DevDoc::Rails::AvoidLifecycleMethodOverride

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb

Overview

Avoid overriding Rails validation/persistence lifecycle methods in model files (‘run_validations!`, `valid?`, etc.).

## Rationale ‘DevDoc/Rails/AvoidRailsCallbacks` bans the callback DSL so lifecycle behaviour stays visible at explicit call sites. Overriding a lifecycle method is a loophole — it reproduces the same hidden control flow without tripping that cop:

# Flagged by AvoidRailsCallbacks:
before_validation :do_something

# NOT flagged by it — but functionally identical, and worse:
def run_validations!
  do_something
  super
end

‘def run_validations!; do_something; super; end` is a `before_validation :do_something` in disguise, and it’s strictly worse:

  • **Silent on typo.** Misspell the method name (drop the ‘!`, or `run_validatons!`) and it’s just a never-called method — no load-time error, no override in effect, behaviour silently reverts to stock Rails. The callback DSL fails loudly (‘NoMethodError`) on a mistyped macro or symbol, caught by the first test that validates.

  • **More obscure.** Overriding an internal Rails method hides the lifecycle hook even more than the DSL it’s avoiding.

This cop pairs with (it does not replace) ‘AvoidRailsCallbacks`: the two cover the two ways to inject lifecycle behaviour — the DSL, and the override.

## What to do instead

  1. Prefer an explicit method at the call site (e.g. ‘save_with_*`) that makes the behaviour visible, or a plain `validate :some_check` where a validation-time check is all you need.

  2. If an override is genuinely required (e.g. a model whose validators read access-gated attributes and must wrap the entire validation run — something ‘validate` can’t express), disable this cop inline with a written justification:

    def run_validations! # rubocop:disable DevDoc/Rails/AvoidLifecycleMethodOverride
      # Reason: <explanation>
      with_access { super }
    end
    

## Not airtight A determined dodge can still ‘prepend` a module, `alias_method`, or use `method_missing`. This cop raises the bar and makes the documented path (inline disable + justification) the easy one; it does not seal every hole.

## Configuration The flagged methods are configurable via ‘Methods`. The default is the validation-lifecycle set — the cleanest 1:1 substitute for the validation callbacks `AvoidRailsCallbacks` bans, with the least false-positive noise. Projects that also want to guard persistence verbs can add `save`/`save!`/`create`/`update`/`destroy` etc.

Examples:

# bad
def run_validations!
  normalize
  super
end

# bad
def valid?(context = nil)
  toggle_access { super }
end

# good — an explicit method / a plain validation declaration
validate :assert_consistent

# good — a non-lifecycle override is fine
def to_param
  slug
end

Constant Summary collapse

MSG =
'Avoid overriding the Rails lifecycle method `%<method>s` — it hides ' \
'callback-like control flow and silently no-ops if mistyped. Prefer an ' \
'explicit method or a `validate` declaration; if an override is genuinely ' \
'required, disable this cop inline with a written justification.'.freeze
DEFAULT_METHODS =
%w[run_validations! valid? invalid? perform_validations].freeze

Instance Method Summary collapse

Instance Method Details

#on_def(node) ⇒ Object



92
93
94
95
96
# File 'lib/rubocop/cop/dev_doc/rails/avoid_lifecycle_method_override.rb', line 92

def on_def(node)
  return unless flagged_methods.include?(node.method_name)

  add_offense(node.loc.name, message: format(MSG, method: node.method_name))
end