Class: RuboCop::Cop::DevDoc::Migration::RequireReferenceForeignKey

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

Overview

Every non-polymorphic reference must carry a truthy foreign_key:.

Rationale

foreign_key: true (or a to_table: hash) is pure referential integrity — never a business decision — so the house rule is that every reference carries the DB-level constraint. Rails defaults references to NO constraint, which means both an omitted option and an explicit foreign_key: false silently ship an unconstrained column, indistinguishable from an oversight.

This cop makes the constraint the enforced default and turns the rare deliberate exception into a documented, greppable site: keep foreign_key: false on the line and disable this cop with a brief reason.

❌ Omitted — reads as "never decided"
t.belongs_to :author, null: false

❌ Explicit false without justification
t.belongs_to :author, null: false, foreign_key: false

✔️
t.belongs_to :author, null: false, foreign_key: true

✔️ Cross-table naming
add_reference :posts, :reviewer, foreign_key: { to_table: :users }

✔️ Deliberate exception, self-documenting
# rubocop:disable DevDoc/Migration/RequireReferenceForeignKey -- append-only audit table
t.belongs_to :snapshot, null: false, foreign_key: false
# rubocop:enable DevDoc/Migration/RequireReferenceForeignKey

Exception

Polymorphic references (polymorphic: true) are skipped — a polymorphic column points at many tables, so no single FK constraint can exist.

NOTE: A reference whose constraint is added separately via add_foreign_key later in the migration (the pattern engine-vendored migrations use) is still flagged — the cop cannot reliably match the two up. Prefer the inline foreign_key: option; for vendored engine migrations, disable with a reason or exclude the file.

NOTE: This cop enforces the constraint's presence; the sibling cop DevDoc/Migration/ExplicitNullOnReference enforces that the column's optionality is stated, 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 :author

# bad
t.belongs_to :author, foreign_key: false

# good
t.belongs_to :author, foreign_key: true

# good (polymorphic — no constraint possible)
t.belongs_to :item, polymorphic: true

Constant Summary collapse

MSG =
'Add `foreign_key: true` (or a `to_table:` hash); a deliberate exception ' \
'keeps `foreign_key: false` plus a reasoned disable of this cop.'.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



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/rubocop/cop/dev_doc/migration/require_reference_foreign_key.rb', line 74

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

  pair = foreign_key_pair(options)
  if pair.nil?
    add_offense(node.loc.selector)
  elsif pair.value.false_type? || pair.value.nil_type?
    add_offense(pair)
  end
end