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

The current branch (impl/scope-type-of) is a first preview. 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, and more. See docs/CURRENT_WORK.md for the full slice trail.

Requirements

  • Nix with the nix-command and flakes features 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. YAMLPsych) 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.
  • self typing — class- and method-body boundaries inject Singleton[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, ...), the prism and rbs gems' RBS, in-source class discovery, and in-source constant value tracking (BUCKETS = [:a, :b, :c]; BUCKETS.firstConstant[:a]).
  • Predicate narrowing — truthiness, nil?, is_a? / kind_of? / instance_of?, finite-literal equality, case-equality (===) for Class / Module / Range / Regexp, case / when integration.
  • 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 check first-preview rules — undefined method on typed receiver, wrong number of positional arguments. Both consult RBS plus in-source def / define_method discovery so reopened classes do not produce false positives.

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/analysisRunner, CheckRules, Diagnostic, FactStore.
  • lib/rigor/inferenceScope-driven typers, dispatchers, and narrowing.
  • sig — RBS signatures for Rigor itself.
  • spec — RSpec test suite (830+ 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):

  1. More rigor check rules (nil-call, type-incompatible writes, unbound locals).
  2. RBS::Extended effect plumbing (%a{rigor:v1:pure}, mutation / escape / call-timing effects).
  3. Diagnostic publication for FallbackTracer events.
  4. Plugin contribution layer.

License

Rigor is licensed under the Mozilla Public License Version 2.0. See LICENSE.