Skip to content

rigor-shoulda-matchers

Validates shoulda-matchers calls inside RSpec.describe <Model> do … end blocks against the model’s real schema: column matchers (validate_presence_of(:col), have_db_column(:col), …) must name a real column, and association matchers (belong_to(:assoc), have_many(:assoc), …) must name a real association of the matching kind. It cross-checks against the :model_index fact published by rigor-activerecord (ADR-9) — so it complements, rather than overlaps, rigor-rspec (which handles RSpec’s own let / subject DSL).

It ships bundled in rigortype. Activate it under plugins: together with rigor-activerecord (which publishes the model index it consumes):

plugins:
- rigor-activerecord # publishes :model_index
- rigor-shoulda-matchers # consumes it
RSpec.describe User do
it { should validate_presence_of(:email) } # OK if `email` is a column
it { should validate_presence_of(:nme) } # warning: unknown column
it { should belong_to(:author) } # OK if `author` is singular
it { should belong_to(:posts) } # warning: kind mismatch (posts is a collection)
it { should have_many(:comments) } # OK if `comments` is a collection
it { should have_many(:nonexistent) } # warning: unknown association
end
RuleSeverityFires when
plugin.shoulda-matchers.unknown-columnwarninga column matcher names a column absent from the model
plugin.shoulda-matchers.unknown-associationwarningan association matcher names an association absent from the model
plugin.shoulda-matchers.association-kind-mismatchwarningthe matcher’s expected kind (singular / collection) disagrees with the association’s actual kind

Column matchers: validate_presence_of / _uniqueness_of / _length_of / _numericality_of / _acceptance_of / _inclusion_of / _exclusion_of / _absence_of / _format_of / _confirmation_of, plus have_db_column / have_db_index. Association matchers and their expected kind: belong_to / have_one (singular), have_many / have_and_belong_to_many (collection). The enclosing describe <Constant> (innermost wins) anchors which model is checked.

Suppress with the qualified rule, e.g. # rigor:disable plugin.shoulda-matchers.unknown-column, or silence the whole family with # rigor:disable plugin.shoulda-matchers.

The plugin has no configuration knobs. When rigor-activerecord is not loaded — or hasn’t published an index for the analysed model — the plugin falls silent; the cross-check is opt-in.

  • No chained-matcher argument validation — the chain terminals on validate_length_of(:col).is_at_most(50), validate_inclusion_of(:col).in_array([...]), etc. are runtime-only.
  • No polymorphic / through: validation — only the named association is checked; the chain modifiers are ignored.
  • No nested-attribute or callback matchers (accept_nested_attributes_for, callback(...)).
  • No route / routing matchers — those are the rigor-rspec-rails domain.

The describe-walker / matcher recogniser and the contract surfaces this plugin exercises (the optional :model_index consume, NodeContext ancestor resolution) are in the plugin’s README. To write a plugin, see examples/ and the rigor-plugin-author skill.

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.