rigortype 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -222
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
  9. data/lib/rigor/analysis/check_rules.rb +275 -44
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
  12. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  13. data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
  14. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  15. data/lib/rigor/analysis/runner.rb +207 -1200
  16. data/lib/rigor/analysis/worker_session.rb +60 -11
  17. data/lib/rigor/bleeding_edge.rb +123 -0
  18. data/lib/rigor/cache/descriptor.rb +86 -8
  19. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  20. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  21. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  22. data/lib/rigor/cache/store.rb +46 -13
  23. data/lib/rigor/cli/annotate_command.rb +100 -15
  24. data/lib/rigor/cli/check_command.rb +708 -0
  25. data/lib/rigor/cli/ci_detector.rb +94 -0
  26. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  27. data/lib/rigor/cli/plugins_command.rb +2 -4
  28. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  29. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  30. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  31. data/lib/rigor/cli/trace_command.rb +143 -0
  32. data/lib/rigor/cli/trace_renderer.rb +310 -0
  33. data/lib/rigor/cli/triage_command.rb +6 -3
  34. data/lib/rigor/cli/triage_renderer.rb +15 -1
  35. data/lib/rigor/cli.rb +21 -612
  36. data/lib/rigor/configuration/severity_profile.rb +13 -1
  37. data/lib/rigor/configuration.rb +66 -7
  38. data/lib/rigor/environment/rbs_loader.rb +78 -68
  39. data/lib/rigor/environment.rb +1 -1
  40. data/lib/rigor/inference/acceptance.rb +10 -0
  41. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  42. data/lib/rigor/inference/budget_trace.rb +29 -2
  43. data/lib/rigor/inference/expression_typer.rb +1080 -105
  44. data/lib/rigor/inference/flow_tracer.rb +180 -0
  45. data/lib/rigor/inference/macro_block_self_type.rb +11 -12
  46. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  47. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  48. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  49. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  50. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  51. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  52. data/lib/rigor/inference/method_dispatcher.rb +187 -55
  53. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  54. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  55. data/lib/rigor/inference/mutation_widening.rb +142 -0
  56. data/lib/rigor/inference/narrowing.rb +330 -37
  57. data/lib/rigor/inference/scope_indexer.rb +770 -39
  58. data/lib/rigor/inference/statement_evaluator.rb +998 -68
  59. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  60. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  61. data/lib/rigor/plugin/base.rb +517 -120
  62. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  63. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  64. data/lib/rigor/plugin/macro.rb +2 -3
  65. data/lib/rigor/plugin/manifest.rb +4 -24
  66. data/lib/rigor/plugin/node_rule_walk.rb +192 -0
  67. data/lib/rigor/plugin/registry.rb +264 -35
  68. data/lib/rigor/plugin.rb +1 -0
  69. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  70. data/lib/rigor/scope/discovery_index.rb +60 -0
  71. data/lib/rigor/scope.rb +199 -204
  72. data/lib/rigor/sig_gen/generator.rb +8 -0
  73. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  74. data/lib/rigor/source/literals.rb +14 -0
  75. data/lib/rigor/triage/catalogue.rb +4 -19
  76. data/lib/rigor/triage.rb +69 -1
  77. data/lib/rigor/type/combinator.rb +34 -0
  78. data/lib/rigor/version.rb +1 -1
  79. data/lib/rigor.rb +0 -1
  80. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  81. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  82. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  83. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  84. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  85. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  86. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
  87. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  88. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
  89. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  90. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  91. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  92. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  93. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  94. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  96. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  97. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  98. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  99. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  100. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
  101. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  102. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  103. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  104. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  105. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  106. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  107. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
  108. data/sig/rigor/analysis/fact_store.rbs +3 -0
  109. data/sig/rigor/environment.rbs +0 -2
  110. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  111. data/sig/rigor/inference.rbs +5 -0
  112. data/sig/rigor/plugin/base.rbs +6 -4
  113. data/sig/rigor/plugin/manifest.rbs +1 -2
  114. data/sig/rigor/scope.rbs +50 -29
  115. data/sig/rigor/source.rbs +1 -0
  116. data/sig/rigor/type.rbs +1 -0
  117. data/sig/rigor.rbs +1 -1
  118. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  119. data/skills/rigor-ci-setup/SKILL.md +319 -0
  120. data/skills/rigor-plugin-author/SKILL.md +6 -4
  121. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  122. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  123. metadata +21 -3
  124. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
  125. data/lib/rigor/plugin/macro/external_file.rb +0 -143
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9379e94547d874f10fc2fcb79e354b16d0459350bdd68a9025dc5bd0ee6bdc3d
4
- data.tar.gz: 4b773c4952be32d673358758340d85a34f9839890eede1669ff863a779b30d69
3
+ metadata.gz: 6e2fede9ea7407238b1f9c8f5784cc43f22724ee86decdf81f4a148c4ddff763
4
+ data.tar.gz: 57e9757b838f29185d9b936ddff586d372126540afe12cefb1e98dbc7e0a6109
5
5
  SHA512:
6
- metadata.gz: '08680ad9e159ff52c031548a45e862539419f0719a251b0b46dc3dddc5313373a7434806024429c1ebf1c580bafcdb7507fed1b2574139c17cb2394129bc0581'
7
- data.tar.gz: 4469fa97e287f84772543d0525866a733fd0e075c486e8a8dc650b09e31a1a02e1b91602d61b0678de9c2fb914bd999a4cea998e0ab6c39f3ee4970892e3b39c
6
+ metadata.gz: 33b889562fefed7516dec175b7d85cc9f725ebcc7d94df30b5a58a436322709517a02bda2a0de10d746004c8c2d198aaa673f12577ddfe3ffeb37405508e1d9c
7
+ data.tar.gz: f5773b727c0fb718df1ab1163add87f0dd7e94913fefd0599e7465e5a40afbfa201b2b14a09eb524b2e98dc6e30bd84ab0424355802ded977386f5a706d84837
data/README.md CHANGED
@@ -4,276 +4,213 @@
4
4
  [![GitHub License](https://img.shields.io/github/license/rigortype/rigor)](https://github.com/rigortype/rigor/blob/master/LICENSE)
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rigortype/rigor)
6
6
 
7
- **Inference-first static analysis for Ruby.** Run `rigor check` over your
8
- code no annotations, no runtime dependency, no DSL.
7
+ **Type-aware bug finding for Ruby zero annotations, and a
8
+ zero-false-positive bar enforced against real codebases.** Run one
9
+ command over the code you already have, and trust every line of
10
+ output.
9
11
 
10
- Rigor parses Ruby with [Prism](https://github.com/ruby/prism), runs a
11
- flow-sensitive type-inference engine over each file, and reports a small
12
- but trustworthy catalogue of bugs: undefined methods on typed receivers,
13
- wrong-argument-count calls, provable divisions by zero, and more.
14
-
15
- **Your team never needs to write type annotations** — Rigor derives
16
- everything from the code itself. An AI Skill gets a project running in
17
- minutes with a configuration tailored to its stack. When you want to go
18
- deeper — tighter checks, framework-aware analysis, your own refinements —
19
- Rigor's type system is remarkably powerful, and a Skill takes you there
20
- step-by-step.
12
+ ```sh
13
+ gem install rigortype && rigor check app lib
14
+ ```
21
15
 
22
- **Three design commitments drive Rigor.**
16
+ (The tool itself runs on Ruby 4.0; your project can stay on whatever
17
+ Ruby it targets — see [Get started](#get-started-in-one-prompt).)
23
18
 
24
- 1. **Types are facts, not wishes.** Hand-written annotations drift the
25
- moment they are written. Rigor infers from the code itself — every
26
- type is derived from what your source actually produces. When you do
27
- want RBS in `sig/`, [`rigor sig-gen`](https://rigor.typedduck.fail/reference/adr/14-rbs-sig-generation/)
28
- emits it from inference results so the written form starts in sync
29
- with reality.
30
- 2. **Your specs are types.** Do you really need to write type
31
- annotations? Your `spec/` is already type information. RSpec
32
- `expect(x).to be_a(T)` / `eq(literal)` assertions contribute
33
- narrowing facts from that point forward; `subject` / `let` bindings
34
- propagate the SUT's type into downstream `it` bodies; factory
35
- definitions in `spec/factories/` feed attribute shapes back into
36
- `rigor-activerecord` and `rigor-factorybot`'s cross-plugin channels.
37
- The test suite you already have becomes a live type oracle.
38
- 3. **Programmable inference beyond unions.** A plain union
39
- (`Integer | nil`) is not the type story Ruby needs. Rigor reasons
40
- about *what values an expression actually produces* — literal values,
41
- integer ranges, refinement carriers, per-position tuple / hash shapes
42
- — and exposes a plugin API plus a
43
- [macro / DSL expansion substrate](https://rigor.typedduck.fail/reference/adr/16-macro-expansion/)
44
- so Rails-shape DSLs become first-class type sources.
19
+ No type annotations to write or maintain, no runtime dependency, no
20
+ changes to your code. Rigor parses Ruby with
21
+ [Prism](https://github.com/ruby/prism) and runs a flow-sensitive
22
+ inference engine that reasons about the *values* your expressions
23
+ produce not just their classes. It catches undefined methods (and
24
+ typos) on inferred receivers, wrong argument counts, receivers that
25
+ can be `nil` on a live path, unreachable branches and `case` clauses,
26
+ and conditions that can only ever be truthy.
45
27
 
46
- ## Installation
28
+ ## Hello, Rigor
47
29
 
48
- Rigor is a tool, not a library install it independently, **not** in
49
- your project's `Gemfile`. It runs on Ruby 4.0, regardless of which Ruby
50
- version your project targets.
30
+ A realistic typo, caught with the receiver's *computed value* in the
31
+ message:
51
32
 
52
- **Using an AI coding agent?** Hand it this prompt and it will detect
53
- your environment, install Rigor, and kick off the project-init Skill
54
- automatically:
33
+ ```ruby
34
+ # demo.rb
35
+ def slug(title)
36
+ title.downcase.gsub(/\s+/, "-")
37
+ end
55
38
 
56
- ```
57
- Install Rigor in this project by following the instructions at
58
- https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
39
+ s = slug("Hello World")
40
+ s.lenght
59
41
  ```
60
42
 
61
- Prefer to set up in your language? Find prompts for Japanese, Chinese,
62
- Korean, Portuguese, Spanish, French, German, Italian, Vietnamese, Thai,
63
- Indonesian, Polish, Ukrainian, Russian, Romanian, and Turkish at
64
- [docs/manual/14-rails-quickstart.md](docs/manual/14-rails-quickstart.md#step-1--install-ruby-40-and-rigor-common-to-both-paths)
65
- (or the [online version](https://rigor.typedduck.fail/reference/manual/14-rails-quickstart/)).
66
-
67
- **Manual install** — the recommended path uses
68
- [`mise`](https://mise.jdx.dev/), which provisions both Ruby 4.0 and
69
- Rigor pinned per project:
70
-
71
- ```sh
72
- mise use ruby@4.0
73
- mise use gem:rigortype
43
+ ```shell-session
44
+ $ rigor check demo.rb
45
+ demo.rb:7:3: error: undefined method `lenght' for "hello-world"
74
46
  ```
75
47
 
76
- If you already have Ruby 4.0 available, `gem install rigortype` works too.
77
- The gem is named `rigortype` (the name `rigor` was taken on RubyGems);
78
- the executable it installs is `rigor`.
79
-
80
- Full options — `asdf`, dev containers, CI workflow template — are in the
81
- [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/)
82
- and [CI guide](https://rigor.typedduck.fail/reference/manual/11-ci/).
48
+ Note what the error says: not ``undefined method `lenght' for String``
49
+ but **`for "hello-world"`** Rigor traced the value through the
50
+ method call and the `downcase.gsub` chain, with no annotations
51
+ anywhere.
83
52
 
84
- ## Getting started with AI Skills
53
+ To see what Rigor sees, run `rigor annotate fact.rb` — it reprints
54
+ the file with the inferred type of every line in the margin. The
55
+ `#=>` comments below are **added by `annotate`**, not part of the
56
+ source:
85
57
 
86
- The fastest path from zero to a running Rigor setup is the
87
- **`rigor-project-init` Skill** — an AI agent skill bundled under
88
- [`skills/`](skills/) that detects your stack, recommends the right plugins,
89
- and writes `.rigor.dist.yml` for you:
58
+ ```ruby
59
+ # fact.rb
60
+ def factorial(n) #=> Integer
61
+ (1..n).reduce(1, :*) #=> Integer
62
+ end #=> :factorial
90
63
 
91
- ```
92
- # In Claude Code or any AI assistant that supports Agent Skills:
93
- Use the rigor-project-init skill
64
+ answer = factorial(5) #=> 120
94
65
  ```
95
66
 
96
- The Skill walks your `Gemfile`, proposes a plugin set matched to your
97
- framework (Rails, Sinatra, dry-rb, …), lets you choose an adoption mode
98
- **baseline** (acknowledge existing diagnostics and work them down
99
- incrementally) or **strict** (zero-diagnostic gate from day one) then
100
- commits a ready-to-use configuration. No manual YAML editing required.
67
+ No signature on `factorial`, yet the method types as `Integer` — and
68
+ at the call site, where the argument is known, Rigor folds the whole
69
+ computation down to the *value* `120`. The same machinery tracks hash
70
+ shapes through your params, string values through builder chains, and
71
+ `nil` through guard clauses in everyday application code. (Output is
72
+ syntax-highlighted — through [`bat`](https://github.com/sharkdp/bat)
73
+ when installed.)
101
74
 
102
- Two companion Skills continue the journey once you are up and running:
75
+ ## Get started in one prompt
103
76
 
104
- | Skill | Use when |
105
- | --- | --- |
106
- | [`rigor-baseline-reduce`](skills/rigor-baseline-reduce/) | Reducing an existing baseline: `rigor triage` prioritisation → site-by-site classification → fix / `# rigor:disable` / open a Rigor issue |
107
- | [`rigor-plugin-author`](skills/rigor-plugin-author/) | Teaching Rigor about a project-specific DSL or framework — authors the plugin gem or project-private plugin |
77
+ The fastest path is to hand your AI coding agent (Claude Code, Cursor,
78
+ or any assistant that follows instructions from a URL) this prompt:
108
79
 
109
- All three Skills follow the [agentskills.io](https://agentskills.io/)
110
- convention and work with any AI assistant that discovers skills in your
111
- project directory.
112
-
113
- ## Quick start
114
-
115
- ```sh
116
- # Check lib/ for bugs.
117
- rigor check lib
118
-
119
- # Drop a starter .rigor.yml.
120
- rigor init
80
+ ```
81
+ Install Rigor in this project by following the instructions at
82
+ https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
83
+ ```
121
84
 
122
- # Print the inferred type at a precise FILE:LINE:COL position.
123
- rigor type-of lib/foo.rb:10:5
85
+ It installs Rigor, then runs the bundled **`rigor-project-init`**
86
+ [Agent Skill](https://agentskills.io/): it walks your `Gemfile`,
87
+ proposes plugins matched to your stack (Rails, Sinatra, dry-rb, …),
88
+ lets you pick an adoption mode — **baseline** (acknowledge existing
89
+ diagnostics, work them down incrementally) or **strict**
90
+ (zero-diagnostic gate from day one) — and commits a ready-to-use
91
+ configuration.
124
92
 
125
- # Summarise the diagnostic stream: distribution, hotspots, heuristic hints.
126
- rigor triage lib
93
+ **This works in your language.** The prompt is plain natural
94
+ language, so you can write it — and run the whole setup conversation
95
+ — in your mother tongue. In Japanese, for example:
127
96
 
128
- # Emit RBS from inference results — review with --diff, write with --write.
129
- rigor sig-gen --diff lib/foo.rb
130
- rigor sig-gen --write lib/foo.rb
97
+ ```
98
+ 次の手順に従って、このプロジェクトに Rigor をインストールしてください:
99
+ https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
131
100
  ```
132
101
 
133
- ### Sample output
102
+ Ready-made prompts — 日本語, 简体中文, 繁體中文, 한국어, Español,
103
+ Português, Français, Deutsch, Italiano, Tiếng Việt, ภาษาไทย,
104
+ Bahasa Indonesia, Polski, Українська, Русский, Română, Türkçe,
105
+ العربية, فارسی — are in
106
+ the [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/#set-up-in-your-language).
134
107
 
135
- ```sh
136
- $ cat /tmp/demo.rb
137
- "hello".no_such_method # undefined method
138
- [1, 2, 3].rotate(1, 2) # wrong number of arguments
108
+ **Manual install** — Rigor is a tool, not a library: install it
109
+ independently, **not** in your project's `Gemfile`. It runs on Ruby
110
+ 4.0 regardless of which Ruby your project targets
111
+ ([`mise`](https://mise.jdx.dev/) provisions both, pinned per project):
139
112
 
140
- $ rigor check /tmp/demo.rb
141
- /tmp/demo.rb:1:9: error: undefined method `no_such_method' for "hello"
142
- /tmp/demo.rb:2:11: error: wrong number of arguments to `rotate' on Array (given 2, expected 0..1)
113
+ ```sh
114
+ mise use ruby@4.0
115
+ mise use gem:rigortype # or: gem install rigortype
143
116
  ```
144
117
 
145
- Diagnostics fire only when the receiver type is statically known and the
146
- evidence is conclusive Rigor never surfaces a warning it cannot prove.
147
-
148
- ### Faster runs through the cache
118
+ The gem is named `rigortype` (the name `rigor` was taken on RubyGems);
119
+ the executable is `rigor`. `asdf`, dev containers, and CI templates are
120
+ in the [installation guide](https://rigor.typedduck.fail/reference/manual/01-installation/)
121
+ and [CI guide](https://rigor.typedduck.fail/reference/manual/11-ci/).
149
122
 
150
- Rigor caches RBS work (loaded environment, type translation, class
151
- hierarchy) under `.rigor/cache/` — the second `rigor check` is
152
- significantly faster than the first. Add `.rigor/` to your `.gitignore`.
123
+ ### Daily commands
153
124
 
154
125
  ```sh
155
- rigor check --cache-stats lib # inspect cache hit/miss
156
- rigor check --clear-cache lib # wipe if you suspect staleness
157
- ```
158
-
159
- ## The type vocabulary
160
-
161
- A vanilla type checker answers "what *class* is this object?" Rigor
162
- answers "what *subset of values* can this expression produce?" — tracking
163
- literal values, integer ranges, refinement carriers, and structural shapes
164
- through the whole analysis, without a single annotation.
165
-
166
- | Carrier | What it records | Example |
167
- | --- | --- | --- |
168
- | **Literal** (`Constant`) | A single Ruby value | `Constant<42>`, `Constant<"hello">`, `Constant<:foo>` |
169
- | **Integer range** | A bounded interval | `positive-int = int<1, max>`, `int<5, 10>` |
170
- | **Refinement** | Base type minus a point, or restricted by a predicate | `non-empty-string = String - ""`, `lowercase-string` |
171
- | **Tuple / HashShape** | Heterogeneous arrays / known-key hashes | `[1, "two"]` → `Tuple[Constant<1>, Constant<"two">]` |
172
- | **Union** | Finite enumerable set of literals | `Constant<:zero> \| Constant<:small> \| Constant<:large>` |
173
- | **`Dynamic[T]`** | Gradual carrier — "could be anything" | `Dynamic[Top]` when proof is unavailable |
174
-
175
- Every narrower carrier **erases to its base class** for RBS interop, so
176
- importing Rigor is a strictly additive change.
177
-
178
- The full type model — constant folding, `Method` bindings, LightweightHKT,
179
- `RBS::Extended` directives — is in the
180
- [handbook](https://rigor.typedduck.fail/reference/handbook/) and
181
- [type specification](https://rigor.typedduck.fail/reference/type-specification/).
182
-
183
- ### Unlocking tighter types with `RBS::Extended`
184
-
185
- When you *want* to express more than RBS allows, attach a
186
- `%a{rigor:v1:…}` annotation in your `sig/` file. The annotation is a
187
- no-op for ordinary RBS tools; Rigor uses it to tighten call-site and
188
- body types.
189
-
190
- ```rbs
191
- class Slug
192
- %a{rigor:v1:return: non-empty-string}
193
- %a{rigor:v1:param: id is non-empty-string}
194
- def normalise: (::String id) -> ::String
195
- end
126
+ rigor check lib # find bugs (caches under .rigor/ — gitignore it)
127
+ rigor init # drop a starter .rigor.yml
128
+ rigor annotate lib/foo.rb # reprint a file with inferred types in the margin
129
+ rigor triage lib # summarise diagnostics: distribution, hotspots
130
+ rigor sig-gen --diff lib/foo.rb # emit RBS from inference (--write to save)
196
131
  ```
197
132
 
198
- This is entirely optional Rigor runs without any annotations.
199
- The directive grammar and the full refinement-name catalogue are in
200
- [`RBS::Extended`](https://rigor.typedduck.fail/reference/type-specification/rbs-extended/)
201
- and [imported built-in types](https://rigor.typedduck.fail/reference/type-specification/imported-built-in-types/).
133
+ In CI, `rigor check` auto-detects the platform and emits native output
134
+ (GitHub annotations, GitLab Code Quality, SARIF, Checkstyle, JUnit,
135
+ TeamCity).
202
136
 
203
- ## Plugins
137
+ ## Three design commitments
204
138
 
205
- Production plugins ship under [`plugins/`](plugins/) for the most common
206
- Ruby frameworks and gems. Activate them in `.rigor.yml`:
139
+ 1. **Types are facts, not wishes.** Hand-written annotations drift the
140
+ moment they are written. Rigor infers from the code itself — every
141
+ type is derived from what your source actually produces. When you do
142
+ want RBS in `sig/`, [`rigor sig-gen`](https://rigor.typedduck.fail/reference/adr/14-rbs-sig-generation/)
143
+ emits it from inference results so the written form starts in sync
144
+ with reality.
145
+ 2. **A false positive is the worst bug.** Your program works; a
146
+ static analyzer that argues otherwise is the thing that is wrong.
147
+ Rigor fires a diagnostic only when the receiver type is statically
148
+ known and the evidence is conclusive — never on a value it merely
149
+ failed to prove things about — and every precision change is gated
150
+ on real OSS codebases (Mastodon, Redmine, GitLab FOSS) before it
151
+ ships. You should never have to write defensive code, or a
152
+ suppression comment, to calm the analyzer down.
153
+ 3. **Programmable inference beyond unions.** A plain union
154
+ (`Integer | nil`) is not the type story Ruby needs. Rigor tracks
155
+ literal values, integer ranges, refinements like
156
+ `non-empty-string`, and per-position tuple / hash shapes — and
157
+ exposes a plugin API plus a
158
+ [macro / DSL expansion substrate](https://rigor.typedduck.fail/reference/adr/16-macro-expansion/)
159
+ so Rails-shape DSLs become first-class type sources.
207
160
 
208
- ```yaml
209
- plugins:
210
- - rigor-activerecord
211
- - rigor-actionpack
212
- - rigor-rspec
213
- ```
161
+ ## Works with your stack
214
162
 
215
- **Rails ecosystem** [`rigor-rails-routes`](plugins/rigor-rails-routes/),
216
- [`rigor-rails-i18n`](plugins/rigor-rails-i18n/),
217
- [`rigor-activerecord`](plugins/rigor-activerecord/),
218
- [`rigor-actionpack`](plugins/rigor-actionpack/),
219
- [`rigor-actionmailer`](plugins/rigor-actionmailer/),
220
- [`rigor-activejob`](plugins/rigor-activejob/),
221
- [`rigor-factorybot`](plugins/rigor-factorybot/),
222
- [`rigor-pundit`](plugins/rigor-pundit/),
223
- [`rigor-sidekiq`](plugins/rigor-sidekiq/).
224
-
225
- **Other frameworks** — [`rigor-sinatra`](plugins/rigor-sinatra/),
226
- [`rigor-devise`](plugins/rigor-devise/),
227
- [`rigor-dry-struct`](plugins/rigor-dry-struct/),
228
- [`rigor-rspec`](plugins/rigor-rspec/),
229
- [`rigor-actioncable`](plugins/rigor-actioncable/).
230
-
231
- **Type-system extensions** — [`rigor-sorbet`](plugins/rigor-sorbet/)
232
- (ingests Sorbet `sig` / `T.let` / RBI files),
233
- [`rigor-statesman`](plugins/rigor-statesman/),
234
- [`rigor-typescript-utility-types`](plugins/rigor-typescript-utility-types/).
235
-
236
- See [`plugins/README.md`](plugins/README.md) for the full catalogue. To
237
- write a plugin for your own DSL or framework, use the
238
- [`rigor-plugin-author`](skills/rigor-plugin-author/) Skill described above.
239
- Plugin-contract walkthroughs (simplified virtual use cases spotlighting
240
- one extension surface each) are under [`examples/`](examples/).
241
-
242
- ## Configuration
243
-
244
- `rigor init` writes a starter `.rigor.yml`. Most projects only need a
245
- handful of lines:
163
+ Production plugins ship in-repo for the common frameworks and gems —
164
+ **Rails** (ActiveRecord, ActionPack, routes, i18n, jobs, mailers),
165
+ **testing** (RSpec, FactoryBot, minitest), **ecosystem** (Sidekiq,
166
+ Devise, Pundit, Sinatra, dry-rb, GraphQL), and **type-system bridges**
167
+ (`rigor-sorbet` ingests Sorbet `sig` / RBI files, so Rigor runs
168
+ alongside an existing Sorbet setup). If you already maintain RBS in
169
+ `sig/` — from Steep or by hand — Rigor reads it as a type source
170
+ out of the box. Activate plugins in `.rigor.yml`:
246
171
 
247
172
  ```yaml
248
173
  paths: [lib, app]
249
- target_ruby: "3.3"
250
174
  plugins:
251
175
  - rigor-activerecord
252
176
  - rigor-actionpack
253
- baseline: .rigor-baseline.yml # when using baseline adoption mode
177
+ - rigor-rspec
254
178
  ```
255
179
 
256
- Common knobs: `disable` (rule IDs to silence project-wide),
257
- `signature_paths` (additional `sig/`-style directories),
258
- `dependencies.source_inference` (opt-in gem-source walk for gems
259
- without RBS). The `rigor-project-init` Skill writes and populates all
260
- of this for you. Full reference on the
261
- [website](https://rigor.typedduck.fail/).
262
-
263
- In-source suppression: `# rigor:disable <rule>` silences a single line;
264
- `# rigor:disable all` suppresses every rule on that line.
180
+ The full catalogue is in [`plugins/README.md`](plugins/README.md); the
181
+ `rigor-project-init` Skill picks the right set for you. To teach Rigor
182
+ your own DSL, the [`rigor-plugin-author`](skills/rigor-plugin-author/)
183
+ Skill authors a plugin step-by-step, and
184
+ [`rigor-baseline-reduce`](skills/rigor-baseline-reduce/) drives the
185
+ baseline down once you are running.
186
+
187
+ ## Going deeper
188
+
189
+ Where a vanilla checker answers "what *class* is this?", Rigor answers
190
+ "what *subset of values* can this expression produce?" — literal
191
+ values (`Constant<"hello">`), integer ranges (`positive-int`),
192
+ refinements (`non-empty-string = String - ""`), tuple and known-key
193
+ hash shapes. Every narrower carrier erases to its base class for RBS
194
+ interop, so adopting Rigor is strictly additive. When you want to
195
+ *declare* more than RBS can say, optional `%a{rigor:v1:…}` annotations
196
+ in `sig/` files tighten types while staying a no-op for ordinary RBS
197
+ tools.
198
+
199
+ The twelve-chapter [handbook](https://rigor.typedduck.fail/reference/handbook/)
200
+ walks the whole type model; the
201
+ [type specification](https://rigor.typedduck.fail/reference/type-specification/)
202
+ and [user manual](https://rigor.typedduck.fail/reference/manual/) are
203
+ the reference companions.
265
204
 
266
205
  ## Status
267
206
 
268
- Current released version: **`v0.1.15`** (2026-05-29). The analyzer is
269
- usable on real Ruby code today; the rule catalogue is deliberately
270
- conservative Rigor's stance is to surface zero false positives while
271
- the inference surface stabilises. The `0.1.x` preview line has been
272
- hardened against real OSS Rails codebases (Mastodon / Redmine / GitLab
273
- FOSS); `v0.2.0` will open the first evaluation release.
274
-
275
- Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
276
- commitments: [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
207
+ Current release: **`v0.1.18`** (2026-06-11), with `v0.2.0` the first
208
+ evaluation release landing shortly. Rigor analyses real Ruby today:
209
+ the `0.1.x` line has been hardened against Mastodon, Redmine, and
210
+ GitLab FOSS, and the deliberately conservative rule catalogue grows
211
+ only as fast as the zero-false-positive bar allows. Release history:
212
+ [`CHANGELOG.md`](CHANGELOG.md) · forward-looking commitments:
213
+ [Roadmap](https://rigor.typedduck.fail/reference/roadmap/).
277
214
 
278
215
  ## Contributing
279
216
 
@@ -56,6 +56,14 @@ module Rigor
56
56
 
57
57
  Result = Data.define(:node, :polarity)
58
58
 
59
+ # ADR-53 Track B — the node classes the shared {RuleWalk}
60
+ # dispatches to this collector, and the context gate under which
61
+ # the walk suppresses it (loop / block bodies — see the envelope
62
+ # above). The legacy walk's `!in_loop_or_block` guard is now the
63
+ # walk's `:loop_or_block` gate, applied before `#visit`.
64
+ NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
65
+ RULE_WALK_GATES = [:loop_or_block].freeze
66
+
59
67
  # @return [Array<Result>] one entry per qualifying
60
68
  # predicate. Empty when the tree carries no firing
61
69
  # predicates.
@@ -64,17 +72,32 @@ module Rigor
64
72
  @results = []
65
73
  end
66
74
 
75
+ # Legacy single-collector walk — kept as the oracle the
76
+ # ADR-53 Track B equivalence harness compares {RuleWalk}
77
+ # against; deleted when Track B completes.
67
78
  def collect(root)
68
79
  walk(root, in_loop_or_block: false)
69
80
  @results.freeze
70
81
  end
71
82
 
83
+ # {RuleWalk} entry point: the per-node logic of the legacy walk,
84
+ # invoked under the same traversal contract. The `context` is
85
+ # unused — the loop / block suppression this collector relied on
86
+ # is the walk's `:loop_or_block` gate now.
87
+ def visit(node, _context = nil)
88
+ collect_predicate(node)
89
+ end
90
+
91
+ def results
92
+ @results.freeze
93
+ end
94
+
72
95
  private
73
96
 
74
97
  def walk(node, in_loop_or_block:)
75
98
  return unless node.is_a?(Prism::Node)
76
99
 
77
- collect_predicate(node) if conditional_node?(node) && !in_loop_or_block
100
+ visit(node) if conditional_node?(node) && !in_loop_or_block
78
101
 
79
102
  child_in_loop_or_block = in_loop_or_block || enters_loop_or_block?(node)
80
103
  node.compact_child_nodes.each { |child| walk(child, in_loop_or_block: child_in_loop_or_block) }
@@ -36,6 +36,15 @@ module Rigor
36
36
  # implicit return treats `def foo; x = 1; end` as
37
37
  # returning `1`, so the trailing write is intentional.
38
38
  class DeadAssignmentCollector
39
+ # ADR-53 Track B — the node classes the shared {RuleWalk}
40
+ # dispatches to this collector. The legacy walk visits EVERY
41
+ # `DefNode` (it descends into all of them, nested defs included,
42
+ # processing each separately — `gather_*` barrier at nested defs
43
+ # so an outer def never sees an inner def's locals), so the
44
+ # collector needs no context gate: it fires at every `def` the
45
+ # shared full DFS reaches, exactly like the legacy walk.
46
+ NODE_CLASSES = [Prism::DefNode].freeze
47
+
39
48
  # Returns `Array<{def_class:, def_name:, write_node:}>`
40
49
  # — one entry per dead-assignment write. Empty when
41
50
  # the tree has no qualifying writes.
@@ -44,11 +53,27 @@ module Rigor
44
53
  @results = []
45
54
  end
46
55
 
56
+ # Legacy single-collector walk — kept as the oracle the ADR-53
57
+ # Track B equivalence harness compares {RuleWalk} against; deleted
58
+ # when Track B completes.
47
59
  def collect(root)
48
60
  walk_for_def_nodes(root)
49
61
  @results.freeze
50
62
  end
51
63
 
64
+ # {RuleWalk} entry point: the legacy walk's per-`def` logic,
65
+ # invoked at every `def` under the shared traversal contract. The
66
+ # `context` is unused — this collector processes all defs.
67
+ def visit(def_node, _context = nil)
68
+ collect_def_assignments(def_node)
69
+ end
70
+
71
+ # The accumulated result, frozen the same way `#collect` returns
72
+ # it — used by {RuleWalk}-driven callers after the walk completes.
73
+ def results
74
+ @results.freeze
75
+ end
76
+
52
77
  private
53
78
 
54
79
  def walk_for_def_nodes(node)
@@ -31,6 +31,18 @@ module Rigor
31
31
  BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
32
32
  private_constant :BARRIER_NODES
33
33
 
34
+ # ADR-53 Track B — the node classes the shared {RuleWalk}
35
+ # dispatches to this collector, and the context gate under which
36
+ # the walk suppresses it. The legacy walk descends only through
37
+ # class / module bodies and `return`s at the first `def` it meets,
38
+ # so it collects ivar writes only from a `def` that sits directly
39
+ # in a class / module body, never one nested in another `def`. On
40
+ # the shared full DFS that prune becomes the `:inside_def` gate;
41
+ # the enclosing class / module name stack the legacy walk threaded
42
+ # as `qualified_prefix` is now `context.qualified_prefix`.
43
+ NODE_CLASSES = [Prism::DefNode].freeze
44
+ RULE_WALK_GATES = [:inside_def].freeze
45
+
34
46
  # Returns `Hash[class_name (String) => Hash[ivar_name
35
47
  # (Symbol) => Array<{node:, type:}>]]`. Empty when the
36
48
  # tree has no qualifying writes.
@@ -39,11 +51,28 @@ module Rigor
39
51
  @accumulator = {}
40
52
  end
41
53
 
54
+ # Legacy single-collector walk — kept as the oracle the ADR-53
55
+ # Track B equivalence harness compares {RuleWalk} against; deleted
56
+ # when Track B completes.
42
57
  def collect(root)
43
58
  walk(root, [])
44
59
  @accumulator.transform_values(&:freeze).freeze
45
60
  end
46
61
 
62
+ # {RuleWalk} entry point: the legacy walk's per-`def` logic,
63
+ # invoked at each class-/module-body `def` under the shared
64
+ # traversal contract. `collect_def_writes`' verbatim body reads the
65
+ # enclosing class prefix from `context.qualified_prefix`.
66
+ def visit(def_node, context)
67
+ collect_def_writes(def_node, context.qualified_prefix)
68
+ end
69
+
70
+ # The accumulated result, frozen the same way `#collect` returns
71
+ # it — used by {RuleWalk}-driven callers after the walk completes.
72
+ def results
73
+ @accumulator.transform_values(&:freeze).freeze
74
+ end
75
+
47
76
  private
48
77
 
49
78
  def walk(node, qualified_prefix)