Rigor
Rigor is a static analyzer for Ruby that aims to provide modern, inference-first type checking without adding type annotations or runtime dependencies to application code.
This first preview ships a usable end-to-end pipeline: parse Ruby
with Prism, build a flow-sensitive type-inference engine
(Rigor::Scope#type_of), drive a project-aware RBS environment,
and surface diagnostics through a small rigor check rule
catalogue.
Status
master is at the fourth preview (v0.0.4). The engine
recognises the bulk of canonical Ruby surface — local variables,
ivars / cvars / globals (intra- and cross-method), self typing,
lexical constant lookup, predicate narrowing
(is_a? / == / === / case-when), block parameter binding,
closure escape, Tuple / HashShape carriers — plus the v0.0.3
constant-folding surface and the v0.0.4 refinement-carrier
catalogue (Type::Difference / Type::Refined / Type::Intersection,
14 imported built-in refinement names through
Builtins::ImportedRefinements, and the symmetric
rigor:v1:return: / rigor:v1:param: / rigor:v1:assert:
RBS::Extended directive routes). See CHANGELOG.md
for the per-release surface and
docs/CURRENT_WORK.md for the live slice
trail.
Requirements
- Nix with the
nix-commandandflakesfeatures available. - Ruby 4.0.x and Bundler 4.x, provided by the Flake development shell.
Setup
nix --extra-experimental-features 'nix-command flakes' develop --command bundle install
nix --extra-experimental-features 'nix-command flakes' develop --command bundle exec rake
For interactive development, enter the Flake shell first:
nix --extra-experimental-features 'nix-command flakes' develop
Quick Start
Inside the Flake shell:
# Show available subcommands.
bundle exec exe/rigor help
# Print the inferred type at FILE:LINE:COL.
bundle exec exe/rigor type-of lib/rigor/scope.rb:55:9
# Report Scope#type_of coverage across a tree.
bundle exec exe/rigor type-scan lib
# Diagnose unknown methods and wrong-arity calls on typed receivers.
bundle exec exe/rigor check lib
# Write a starter .rigor.yml.
bundle exec exe/rigor init
Example diagnostics
rigor check reports the canonical type-check signals it can
prove against the loaded RBS environment:
$ cat /tmp/demo.rb
"hello".no_such_method # bug: undefined method
[1, 2, 3].rotate(1, 2) # bug: wrong number of arguments
$ bundle exec exe/rigor check /tmp/demo.rb
/tmp/demo.rb:1:9: error: undefined method `no_such_method' for "hello"
/tmp/demo.rb:2:11: error: wrong number of arguments to `rotate' on Array (given 2, expected 0..1)
The rule catalogue is intentionally narrow: a diagnostic fires
only when the receiver type is statically known and the method
set on that class is enumerable through RBS or in-source def /
define_method discovery. Implicit-self calls, dynamic
receivers, and constant-decl alias classes (e.g. YAML → Psych)
are skipped to avoid false positives.
What works
The first preview engine resolves:
- Local / instance / class / global variables — intra-method
bindings (
@x = 1; @x), cross-method ivar / cvar accumulators (def init; @x = 1; end; def get; @x; end), program-wide globals. - Compound writes —
||=,&&=,+=and friends thread through scope for every variable kind. selftyping — class- and method-body boundaries injectSingleton[T]/Nominal[T]; implicit-self call dispatch routes through the enclosing class's RBS.- Constant lookup — lexical walk against
scope.self_type, RBS-core, common stdlib (pathname,optparse,json,yaml, ...), theprismandrbsgems' RBS, in-source class discovery, and in-source constant value tracking (BUCKETS = [:a, :b, :c]; BUCKETS.first→Constant[:a]). - Predicate narrowing — truthiness,
nil?,is_a?/kind_of?/instance_of?, finite-literal equality, case-equality (===) for Class / Module / Range / Regexp,case/whenintegration. - Blocks — parameter binding (incl. destructuring + numbered
parameters), block-return-type uplift through generic methods
(
Array#map { |n| n.to_s }→Array[String]), closure escape classification, captured-local invalidation on escaping blocks. - Tuple / HashShape carriers — shape-aware element access,
range / start-length slices, closed / open / required / optional
policies threaded through
Acceptance. rigor checkfirst-preview rules — undefined method on typed receiver, wrong number of positional arguments. Both consult RBS plus in-sourcedef/define_methoddiscovery so reopened classes do not produce false positives.RBS::Extendedannotation routes —rigor:v1:return: <refinement>overrides a method's RBS-declared return;rigor:v1:param: <name> [is] <refinement>tightens a parameter at both the call boundary and inside the method body;rigor:v1:assert <name> is <refinement>substitutes the refinement carrier at the post-call scope. The right-hand side accepts any Capitalised class name OR a kebab-case refinement payload from the imported-built-in catalogue (including parameterised formsnon-empty-array[Integer]and bounded rangesint<5, 10>).
See docs/CURRENT_WORK.md for the canonical status snapshot, docs/internal-spec/inference-engine.md for the engine contract, and docs/adr/ for the decision records.
Project layout
lib/rigor— runtime library, type model, inference engine, CLI.lib/rigor/analysis—Runner,CheckRules,Diagnostic,FactStore.lib/rigor/inference—Scope-driven typers, dispatchers, and narrowing.sig— RBS signatures for Rigor itself.spec— RSpec test suite (1250+ examples).docs/adr— architecture decision records.docs/internal-spec— engine contracts.docs/type-specification— type-language semantics.
Roadmap past first preview
In rough priority order (see CURRENT_WORK.md for details):
- More
rigor checkrules (nil-call, type-incompatible writes, unbound locals). RBS::Extendedeffect plumbing (%a{rigor:v1:pure}, mutation / escape / call-timing effects).- Diagnostic publication for
FallbackTracerevents. - Plugin contribution layer.
License
Rigor is licensed under the Mozilla Public License Version 2.0. See LICENSE.