Skip to content

rigor-activerecord

Types ActiveRecord finder and relation calls against your project’s db/schema.rb and discovered model classes — so User.find(1) is User, User.where(emial: …) is flagged as an unknown column, and user.posts carries its element type through the chain. The plugin reads source only; it never loads active_record, so Rigor stays decoupled from Rails.

It ships bundled in rigortype — no separate install. Activate it under plugins: in your config file:

plugins:
- rigor-activerecord
demo.rb:20:1: info: `User.find` returns User (table: `users`) [plugin.activerecord.model-call]
demo.rb:23:1: info: `User.where` (:admin) on table `users` [plugin.activerecord.model-call]
errors_demo.rb:13:1: error: `User.where(emial: ...)` references unknown column `emial` on table `users` (did you mean `:email`?) [plugin.activerecord.unknown-column]
errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plugin.activerecord.wrong-arity]
DiagnosticSeverityRule
Recognised Model.find / Model.find_by / Model.where call:infoplugin.activerecord.model-call
Model.find_by(unknown: ...) / Model.where(unknown: ...):errorplugin.activerecord.unknown-column
Model.find with 0 args:errorplugin.activerecord.wrong-arity
db/schema.rb not readable:warningplugin.activerecord.load-error

Did-you-mean suggestions use Levenshtein distance ≤ 3 against the resolved table’s column names.

plugins:
- gem: rigor-activerecord
config:
schema_file: "db/schema.rb" # default
model_search_paths: ["app/models"] # default
model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"] # default

All three keys are optional. Tweak them when:

  • the schema lives elsewhere (schema_file: "shared/db/schema.rb");
  • models are in a non-standard directory (model_search_paths: ["domain/models", "engines/billing/app/models"]);
  • the base class is custom (model_base_classes: ["DbRecord", "ApplicationRecord"]).

The plugin contributes call-site types as well as diagnostics. Class-side: User.find(1)User, User.find_by(...)User | nil, User.find_by!(...) → non-nullable User. Instance-side: a column read (user.name) narrows to the column’s value type, user.admin? to bool, and a singular association (post.user) to the target model.

Relation-returning call sites — User.where(...), User.all, User.order(...), a has_many / has_and_belongs_to_many accessor (user.posts), and user-declared scopes (Post.published) — narrow to ActiveRecord::Relation[Model]. Chained query methods keep the element type, and iteration (user.posts.each { |p| ... }) yields the model. A user-defined scope invoked on a typed relation (User.where(...).published) never surfaces a false call.undefined-method.

  • Direct-superclass match only. class Admin < User where User < ApplicationRecord is not discovered. Either add User to model_base_classes, or list every concrete model explicitly.
  • db/schema.rb only. db/structure.sql (raw SQL dumps) is not supported in this iteration.
  • Column reads, not setters. The plugin types instance-side column reads (user.name, user.admin?) and singular associations, but not the name= setter or the dirty-tracking family (name_changed?, name_was, …).
  • Project-custom inflections aren’t read yet. Model↔table pluralization goes through the real ActiveSupport inflector (so Person → people, Mouse → mice resolve), but rules you declare in config/initializers/inflections.rb are not yet ingested — a model relying on one needs self.table_name (ADR-39 slice 3).

Architecture (the cached schema-parser → model-index → analyzer chain), the source layout, how to run the demo, and the plugin contract surfaces this plugin exercises are documented in the plugin’s README. To write a plugin of your own, see the examples/ walkthroughs and the rigor-plugin-author skill.

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