rigortype 0.1.18 → 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.
- checksums.yaml +4 -4
- data/README.md +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +266 -63
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
- data/lib/rigor/analysis/runner.rb +58 -21
- data/lib/rigor/analysis/worker_session.rb +21 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +3 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +57 -1
- data/lib/rigor/environment/rbs_loader.rb +25 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1052 -43
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +72 -1
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +270 -37
- data/lib/rigor/inference/scope_indexer.rb +696 -25
- data/lib/rigor/inference/statement_evaluator.rb +963 -16
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/base.rb +235 -79
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +12 -11
- data/lib/rigor/scope/discovery_index.rb +2 -0
- data/lib/rigor/scope.rb +132 -6
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +25 -0
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +10 -1
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +7 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e2fede9ea7407238b1f9c8f5784cc43f22724ee86decdf81f4a148c4ddff763
|
|
4
|
+
data.tar.gz: 57e9757b838f29185d9b936ddff586d372126540afe12cefb1e98dbc7e0a6109
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33b889562fefed7516dec175b7d85cc9f725ebcc7d94df30b5a58a436322709517a02bda2a0de10d746004c8c2d198aaa673f12577ddfe3ffeb37405508e1d9c
|
|
7
|
+
data.tar.gz: f5773b727c0fb718df1ab1163add87f0dd7e94913fefd0599e7465e5a40afbfa201b2b14a09eb524b2e98dc6e30bd84ab0424355802ded977386f5a706d84837
|
data/README.md
CHANGED
|
@@ -4,278 +4,213 @@
|
|
|
4
4
|
[](https://github.com/rigortype/rigor/blob/master/LICENSE)
|
|
5
5
|
[](https://deepwiki.com/rigortype/rigor)
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
wrong-argument-count calls, provable divisions by zero, and more.
|
|
12
|
+
```sh
|
|
13
|
+
gem install rigortype && rigor check app lib
|
|
14
|
+
```
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
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).)
|
|
21
18
|
|
|
22
|
-
|
|
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.
|
|
23
27
|
|
|
24
|
-
|
|
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.
|
|
45
|
-
|
|
46
|
-
## Installation
|
|
28
|
+
## Hello, Rigor
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
version your project targets.
|
|
30
|
+
A realistic typo, caught with the receiver's *computed value* in the
|
|
31
|
+
message:
|
|
51
32
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
```ruby
|
|
34
|
+
# demo.rb
|
|
35
|
+
def slug(title)
|
|
36
|
+
title.downcase.gsub(/\s+/, "-")
|
|
37
|
+
end
|
|
55
38
|
|
|
39
|
+
s = slug("Hello World")
|
|
40
|
+
s.lenght
|
|
56
41
|
```
|
|
57
|
-
Install Rigor in this project by following the instructions at
|
|
58
|
-
https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
|
|
59
|
-
```
|
|
60
|
-
|
|
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
42
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
the
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
58
|
+
```ruby
|
|
59
|
+
# fact.rb
|
|
60
|
+
def factorial(n) #=> Integer
|
|
61
|
+
(1..n).reduce(1, :*) #=> Integer
|
|
62
|
+
end #=> :factorial
|
|
90
63
|
|
|
64
|
+
answer = factorial(5) #=> 120
|
|
91
65
|
```
|
|
92
|
-
# In Claude Code or any AI assistant that supports Agent Skills:
|
|
93
|
-
Use the rigor-project-init skill
|
|
94
|
-
```
|
|
95
|
-
|
|
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.
|
|
101
66
|
|
|
102
|
-
|
|
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.)
|
|
103
74
|
|
|
104
|
-
|
|
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 |
|
|
75
|
+
## Get started in one prompt
|
|
108
76
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
project directory.
|
|
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:
|
|
112
79
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
rigor
|
|
97
|
+
```
|
|
98
|
+
次の手順に従って、このプロジェクトに Rigor をインストールしてください:
|
|
99
|
+
https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
|
|
131
100
|
```
|
|
132
101
|
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
[
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
113
|
+
```sh
|
|
114
|
+
mise use ruby@4.0
|
|
115
|
+
mise use gem:rigortype # or: gem install rigortype
|
|
143
116
|
```
|
|
144
117
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
|
156
|
-
rigor
|
|
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)
|
|
157
131
|
```
|
|
158
132
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
196
|
-
```
|
|
133
|
+
In CI, `rigor check` auto-detects the platform and emits native output
|
|
134
|
+
(GitHub annotations, GitLab Code Quality, SARIF, Checkstyle, JUnit,
|
|
135
|
+
TeamCity).
|
|
197
136
|
|
|
198
|
-
|
|
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/).
|
|
137
|
+
## Three design commitments
|
|
202
138
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
plugins:
|
|
210
|
-
- rigor-activerecord
|
|
211
|
-
- rigor-actionpack
|
|
212
|
-
- rigor-rspec
|
|
213
|
-
```
|
|
161
|
+
## Works with your stack
|
|
214
162
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
177
|
+
- rigor-rspec
|
|
254
178
|
```
|
|
255
179
|
|
|
256
|
-
|
|
257
|
-
`
|
|
258
|
-
`
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
`v0.2.0` will open the first evaluation release.
|
|
276
|
-
|
|
277
|
-
Release history: [`CHANGELOG.md`](CHANGELOG.md). Forward-looking
|
|
278
|
-
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/).
|
|
279
214
|
|
|
280
215
|
## Contributing
|
|
281
216
|
|
|
@@ -57,8 +57,12 @@ module Rigor
|
|
|
57
57
|
Result = Data.define(:node, :polarity)
|
|
58
58
|
|
|
59
59
|
# ADR-53 Track B — the node classes the shared {RuleWalk}
|
|
60
|
-
# dispatches to this collector
|
|
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`.
|
|
61
64
|
NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
|
|
65
|
+
RULE_WALK_GATES = [:loop_or_block].freeze
|
|
62
66
|
|
|
63
67
|
# @return [Array<Result>] one entry per qualifying
|
|
64
68
|
# predicate. Empty when the tree carries no firing
|
|
@@ -77,8 +81,10 @@ module Rigor
|
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
# {RuleWalk} entry point: the per-node logic of the legacy walk,
|
|
80
|
-
# invoked under the same traversal contract.
|
|
81
|
-
|
|
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)
|
|
82
88
|
collect_predicate(node)
|
|
83
89
|
end
|
|
84
90
|
|
|
@@ -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)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
module CheckRules
|
|
8
|
+
# ADR-53 Track B (slice B3c) — hosts the main per-node check-rule
|
|
9
|
+
# pass on the shared {RuleWalk} instead of its own
|
|
10
|
+
# `Source::NodeWalker.each` traversal.
|
|
11
|
+
#
|
|
12
|
+
# The main pass is the stateless half of the catalogue: each rule
|
|
13
|
+
# reads only `scope_index[node]` and decides, with NO loop / block
|
|
14
|
+
# suppression and NO threaded context — so the collector declares no
|
|
15
|
+
# `RULE_WALK_GATES` and ignores the walk's `context`.
|
|
16
|
+
#
|
|
17
|
+
# The per-node dispatch itself stays in `CheckRules` (the verbatim
|
|
18
|
+
# `case` from `diagnose`'s former inline walk — only the traversal
|
|
19
|
+
# moved, ADR-53 WD4) and is handed in as a callable built in the
|
|
20
|
+
# `CheckRules` module context, so its calls to the private
|
|
21
|
+
# diagnostic builders remain implicit-self. The collector only owns
|
|
22
|
+
# the traversal hook and accumulation. Unlike the fact collectors
|
|
23
|
+
# its `#results` are the accumulated {Diagnostic}s, in the same
|
|
24
|
+
# emission order the inline `NodeWalker.each` produced them (the
|
|
25
|
+
# shared walk is the same visit-before-descend DFS over
|
|
26
|
+
# `compact_child_nodes`).
|
|
27
|
+
class MainPassCollector
|
|
28
|
+
# The node classes the former inline pass branched on. A plain
|
|
29
|
+
# `Prism::IfNode` covers ternaries and postfix `if` too (the
|
|
30
|
+
# legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same).
|
|
31
|
+
NODE_CLASSES = [
|
|
32
|
+
Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# @param node_diagnostics [#call] maps a `Prism::Node` to the
|
|
36
|
+
# array of diagnostics the main pass emits for it.
|
|
37
|
+
def initialize(node_diagnostics)
|
|
38
|
+
@node_diagnostics = node_diagnostics
|
|
39
|
+
@diagnostics = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# {RuleWalk} entry point: the per-node logic of the former inline
|
|
43
|
+
# `NodeWalker.each` `case`, invoked under the shared traversal.
|
|
44
|
+
def visit(node, _context = nil)
|
|
45
|
+
@diagnostics.concat(@node_diagnostics.call(node))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def results
|
|
49
|
+
@diagnostics
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|