Class: RuboCop::Cop::DevDoc::Migration::ExplicitNullOnReference

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

Overview

Every reference must state null: explicitly.

Rationale

For regular columns the house rule is nullable (DevDoc/Migration/AvoidNonNull), so an absent null: is unambiguous — silence IS the decision. Foreign-key columns are the exception that AvoidNonNull deliberately carves out: optional and mandatory are both completely legitimate, a genuine per-reference business decision. But a legitimate decision that is never written down is indistinguishable from an omission — a bare reference reads as "did the developer forget?".

This cop requires the decision to be stated. Either value passes; null: true is not redundant here, it is the self-documenting spelling of "optional by design".

❌ Ambiguous — optional by design, or forgotten?
t.belongs_to :organization, foreign_key: true

✔️ Mandatory, stated
t.belongs_to :organization, foreign_key: true, null: false

✔️ Optional, stated
t.belongs_to :organization, foreign_key: true, null: true

Polymorphic references are held to the same rule — their optionality is just as much a decision.

Relationship with Rails/NotNullColumn

The stock cop flags add_reference ... null: false (NOT NULL added to a possibly-populated table without a default) — the exact line complying with THIS cop produces for a mandatory FK, so the two collide. Consumers disable Rails/NotNullColumn: its add_column half is covered more strictly by DevDoc/Migration/AvoidNonNull, and its add_reference half contradicts house doctrine (a populated-table migration fails loudly at migrate time and uses the add-nullable -> backfill -> tighten pattern instead). This gem's default config disables it, but that propagation is load-order dependent (see NoBulkChangeTable's docstring) — disable it in the project's .rubocop.yml too.

NOTE: This cop enforces that optionality is stated; the sibling cop DevDoc/Migration/RequireReferenceForeignKey enforces the constraint's presence, and DevDoc/Migration/RedundantReferenceIndex removes the one reference option whose default is already correct. Together: everything written on a reference is a decision, everything omitted is doctrine.

Examples:

# bad
t.belongs_to :user, foreign_key: true

# good
t.belongs_to :user, foreign_key: true, null: false

# good
t.belongs_to :user, foreign_key: true, null: true

Constant Summary collapse

MSG =
'State `null:` explicitly — optional and mandatory are both legitimate ' \
'for a reference, so omission reads as an unmade decision.'.freeze
RESTRICT_ON_SEND =
%i[belongs_to references add_reference add_belongs_to].freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object



69
70
71
72
73
74
# File 'lib/rubocop/cop/dev_doc/migration/explicit_null_on_reference.rb', line 69

def on_send(node)
  options = node.arguments.find(&:hash_type?)
  return if options && null_stated?(options)

  add_offense(node.loc.selector)
end