rigortype 0.2.0 → 0.2.2
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 +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +120 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Appendix — Coming from TypeProf
|
|
2
|
+
|
|
3
|
+
[TypeProf](https://github.com/ruby/typeprof) is Ruby's
|
|
4
|
+
official type *inference* tool — a type-level abstract
|
|
5
|
+
interpreter, maintained inside Ruby core, that reads
|
|
6
|
+
un-annotated `.rb` files and tells you the types it deduced.
|
|
7
|
+
If you have used TypeProf, the most important thing to know is
|
|
8
|
+
that **Rigor and TypeProf share TypeProf's headline promise**:
|
|
9
|
+
neither one asks you to write `.rbs` first. Where the Steep
|
|
10
|
+
appendix opens with "both consume the same RBS," this one opens
|
|
11
|
+
with the inverse: both *produce* type information from plain
|
|
12
|
+
Ruby. The interesting differences are in *how* they infer and
|
|
13
|
+
*what they do with the result*.
|
|
14
|
+
|
|
15
|
+
This appendix is for users who already think in TypeProf terms
|
|
16
|
+
and want to know which Rigor concept matches which TypeProf
|
|
17
|
+
concept.
|
|
18
|
+
|
|
19
|
+
## The five-second pitch
|
|
20
|
+
|
|
21
|
+
| Question | TypeProf | Rigor |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| Primary job | Generate RBS prototypes from Ruby | Check Ruby for provable bugs (`rigor check`) |
|
|
24
|
+
| Needs `.rbs` to start? | No — infers from `.rb` | No — infers from `.rb` |
|
|
25
|
+
| Inference strategy | Whole-program abstract interpretation ("type-level execution") from entry points | Local, per-method inference with budgets + a catalog at boundaries |
|
|
26
|
+
| Scale target | Small files / a prototyping pass | Whole codebase, incrementally, cached |
|
|
27
|
+
| Default output | RBS signatures (+ some errors as a side effect) | Diagnostics (+ RBS on demand via `sig-gen`) |
|
|
28
|
+
| Diagnostic philosophy | Report what abstract interpretation stumbles on | Stay silent unless the bug is provable |
|
|
29
|
+
| Literal precision | Widens to nominal in output (`1` → `Integer`) | Keeps `Constant<1>`, refinements, `IntegerRange` |
|
|
30
|
+
|
|
31
|
+
If TypeProf's slogan is "run your Ruby at the type level and
|
|
32
|
+
write down what came back," Rigor's is "prove what you can,
|
|
33
|
+
flag only that, and scale it." The two overlap most exactly at
|
|
34
|
+
one feature — `rigor sig-gen` (Chapter 11) does the job
|
|
35
|
+
TypeProf's CLI was built for.
|
|
36
|
+
|
|
37
|
+
## Both infer without annotations — that is the common ground
|
|
38
|
+
|
|
39
|
+
TypeProf and Rigor are the two Ruby
|
|
40
|
+
tools that give you something useful on a `lib/` directory
|
|
41
|
+
with **zero `.rbs` files**. Steep, by contrast, expects the
|
|
42
|
+
signatures up front. So if you came to Rigor from TypeProf,
|
|
43
|
+
the "no annotations needed" stance (Chapter 1) will already
|
|
44
|
+
feel familiar — it is the assumption you have been working
|
|
45
|
+
under all along.
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# slug.rb — no sig/ directory anywhere
|
|
49
|
+
class Slug
|
|
50
|
+
def normalise(raw)
|
|
51
|
+
raw.strip.downcase
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
TypeProf abstractly interprets `normalise`, sees `String#strip`
|
|
57
|
+
then `String#downcase`, and (given a call site that passes a
|
|
58
|
+
`String`) emits `def normalise: (String) -> String`. Rigor
|
|
59
|
+
infers the same body locally and, under `sig-gen`, emits the
|
|
60
|
+
same signature — while *also* knowing internally that the
|
|
61
|
+
result is a `non-empty?`-unconstrained but lower-cased string
|
|
62
|
+
carrier, which it erases to `String` at the boundary.
|
|
63
|
+
|
|
64
|
+
Where they diverge:
|
|
65
|
+
|
|
66
|
+
- **TypeProf** follows the call graph. To learn that `raw` is
|
|
67
|
+
a `String`, it needs to *see a call* that passes one — so
|
|
68
|
+
TypeProf is most useful when pointed at a program with an
|
|
69
|
+
entry point (or a small harness that exercises the methods).
|
|
70
|
+
- **Rigor** infers each method on its own terms against a
|
|
71
|
+
catalog of core/stdlib/gem types, bounded by inference
|
|
72
|
+
budgets, and falls back to `Dynamic[top]` for a parameter it
|
|
73
|
+
cannot pin — no entry point or harness required.
|
|
74
|
+
|
|
75
|
+
## Type vocabulary — TypeProf output vs Rigor carriers
|
|
76
|
+
|
|
77
|
+
TypeProf emits RBS, so its *output* vocabulary is RBS. Rigor
|
|
78
|
+
reads and erases to the same RBS. The difference is internal
|
|
79
|
+
precision: TypeProf widens literals on the way out; Rigor
|
|
80
|
+
keeps a richer carrier and only erases at the boundary.
|
|
81
|
+
|
|
82
|
+
| Ruby expression | TypeProf output | Rigor internal (display) |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `1` | `Integer` | `Constant<1>` (erases: `Integer`) |
|
|
85
|
+
| `"foo".upcase` | `String` | `Constant<"FOO">` (erases: `String`) |
|
|
86
|
+
| `[1, "a"]` | `[Integer, String]` | `Tuple[Constant<1>, Constant<"a">]` |
|
|
87
|
+
| `{name: "x", age: 1}` | `{name: String, age: Integer}` | `HashShape{name: …, age: …}` |
|
|
88
|
+
| `x` unknown | `untyped` | `Dynamic[top]` (display: `untyped`) |
|
|
89
|
+
| `nil`-or-`Integer` | `Integer?` | `Integer \| Constant<nil>` (display: `Integer?`) |
|
|
90
|
+
| a `> 0`-guarded int | `Integer` | `positive-int` refinement (erases: `Integer`) |
|
|
91
|
+
|
|
92
|
+
The practical upshot: feed the same file to both, and
|
|
93
|
+
TypeProf's RBS and `rigor sig-gen`'s RBS will usually agree at
|
|
94
|
+
the *declaration* level, because Rigor's
|
|
95
|
+
[RBS erasure](../type-specification/rbs-erasure.md) deliberately
|
|
96
|
+
discards exactly the extra precision (`Constant`, `Refined`,
|
|
97
|
+
`IntegerRange`) that TypeProf never tracked. Inside the
|
|
98
|
+
analyzer, Rigor is carrying more — which is what powers its
|
|
99
|
+
refinement-carrier narrowing and constant-folding diagnostics.
|
|
100
|
+
(TypeProf does its own flow-sensitive narrowing on type-identity
|
|
101
|
+
predicates like `is_a?` / `nil?`; what it does not carry is the
|
|
102
|
+
*value-predicate refinement* layer or the curated diagnostics
|
|
103
|
+
Rigor produces from it.)
|
|
104
|
+
|
|
105
|
+
## Analysis model — the largest conceptual difference
|
|
106
|
+
|
|
107
|
+
This is where the tools part ways.
|
|
108
|
+
|
|
109
|
+
**TypeProf is a whole-program abstract interpreter.** It walks
|
|
110
|
+
the program from its entry points and *executes it at the type
|
|
111
|
+
level*: every method call propagates abstract argument types
|
|
112
|
+
to the callee, the callee's body is interpreted, and return
|
|
113
|
+
types flow back. This is inter-procedural and remarkably
|
|
114
|
+
precise on small programs — TypeProf can infer a method's
|
|
115
|
+
parameter types purely from how it is *called*, which neither
|
|
116
|
+
Steep nor Rigor attempts.
|
|
117
|
+
|
|
118
|
+
The cost is scale. Abstract interpretation of a whole program
|
|
119
|
+
is expensive, and the analysis can blow up combinatorially on
|
|
120
|
+
large or highly polymorphic codebases. TypeProf is explicitly
|
|
121
|
+
positioned as a tool for small programs, single files, or a
|
|
122
|
+
*starting-point* RBS pass — not a checker you run over a
|
|
123
|
+
100k-line app on every save.
|
|
124
|
+
|
|
125
|
+
**Rigor is a local, budgeted inferrer with a catalog at the
|
|
126
|
+
boundary.** It infers one method at a time. When that method
|
|
127
|
+
calls another, Rigor consults a *catalog* of known return
|
|
128
|
+
types (core, stdlib, gem RBS, plugin contributions) rather
|
|
129
|
+
than re-interpreting the callee's body. Calls it cannot
|
|
130
|
+
resolve become `Dynamic[top]` and stop there. This is less
|
|
131
|
+
precise than TypeProf on a self-contained toy program — Rigor
|
|
132
|
+
will not deduce a parameter type from a single call site the
|
|
133
|
+
way TypeProf can — but it is bounded by
|
|
134
|
+
[inference budgets](../type-specification/inference-budgets.md)
|
|
135
|
+
and backed by per-file caching (ADR-6), so it stays usable on
|
|
136
|
+
a whole codebase and across runs.
|
|
137
|
+
|
|
138
|
+
| | TypeProf | Rigor |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| Unit of analysis | Whole program from entry points | One method at a time |
|
|
141
|
+
| Cross-method types | Re-interprets the callee body | Looks up the callee in a catalog |
|
|
142
|
+
| Infers params from call sites? | Yes (its signature move) | No (params default to `Dynamic[top]`) |
|
|
143
|
+
| Bounded? | By practical blow-up limits | By explicit inference budgets |
|
|
144
|
+
| Incremental / cached? | TypeProf 2 / `--lsp` improves this | Per-file cache across runs + machines |
|
|
145
|
+
|
|
146
|
+
The trade is intentional: TypeProf buys precision on
|
|
147
|
+
small inputs with whole-program interpretation; Rigor buys
|
|
148
|
+
scale and silence with local inference and a catalog.
|
|
149
|
+
|
|
150
|
+
## RBS generation — `typeprof` CLI vs `rigor sig-gen`
|
|
151
|
+
|
|
152
|
+
This is the one feature where the tools do the *same job*, and
|
|
153
|
+
it is worth a direct comparison. Generating RBS from Ruby is
|
|
154
|
+
TypeProf's entire reason to exist; for Rigor it is one
|
|
155
|
+
secondary command, [Chapter 11](11-sig-gen.md).
|
|
156
|
+
|
|
157
|
+
| | `typeprof foo.rb` | `rigor sig-gen` |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| Output | RBS to stdout | RBS via `--print` / `--diff` / `--write` |
|
|
160
|
+
| Param types | Inferred from call sites | Conservative; `--params` policy, ADR-5 trade-off |
|
|
161
|
+
| Existing sigs | Regenerates from scratch | `new-file` / `new-method` / `tighter-return` classification; never overwrites a tighter human return |
|
|
162
|
+
| Literal precision in output | Widened to nominal | Erased to RBS (same widening at the boundary) |
|
|
163
|
+
| Driving signal it feeds back | A one-shot prototype to hand-edit | Gaps in `sig-gen` are themselves the signal Rigor should infer better |
|
|
164
|
+
|
|
165
|
+
A note specific to *this* repository: the project's standing
|
|
166
|
+
policy (AGENTS.md § "RBS Authorship") is to prefer
|
|
167
|
+
`rigor sig-gen` over hand- or AI-authored RBS, precisely
|
|
168
|
+
*because* a gap in `sig-gen` output is more valuable feedback
|
|
169
|
+
than a patched-over signature. If you are used to running
|
|
170
|
+
`typeprof` and pasting its output into `sig/`, the Rigor
|
|
171
|
+
analogue is `rigor sig-gen --diff` — but the mindset shifts
|
|
172
|
+
from "scaffold then edit" to "if the scaffold is wrong, fix
|
|
173
|
+
the inference."
|
|
174
|
+
|
|
175
|
+
## Tests as inference fuel — the bidirectional question
|
|
176
|
+
|
|
177
|
+
A natural follow-on to the analysis-model section: if TypeProf
|
|
178
|
+
learns parameter types from call sites, and tests are nothing
|
|
179
|
+
*but* call sites, how do the two tools use your `spec/` (or
|
|
180
|
+
`test/`) suite? There are two directions, and the tools
|
|
181
|
+
differ on each.
|
|
182
|
+
|
|
183
|
+
### Direction 1 — tests → method types
|
|
184
|
+
|
|
185
|
+
Tests are a pile of concrete examples of how a method is
|
|
186
|
+
called, so they are evidence for parameter types. This is
|
|
187
|
+
where the analysis models show through most sharply.
|
|
188
|
+
|
|
189
|
+
- **TypeProf fuels on call sites — and a test is just one
|
|
190
|
+
kind of call site.** TypeProf has no concept of "a test." It
|
|
191
|
+
infers parameters by abstractly interpreting the whole
|
|
192
|
+
program, so *any* call it can see feeds a parameter type:
|
|
193
|
+
top-level code, an example under `__END__`, a throwaway
|
|
194
|
+
driver, or a line in a spec are all the same to it. A call
|
|
195
|
+
that runs `Foo.new.bar(42)` — wherever it lives — is how
|
|
196
|
+
TypeProf learns `bar` takes an `Integer`; with no such call,
|
|
197
|
+
the parameter stays `untyped`. Tests happen to be a rich
|
|
198
|
+
*source* of call sites (so pointing TypeProf at exercising
|
|
199
|
+
code, including a suite, helps), but TypeProf neither
|
|
200
|
+
recognises nor privileges them as tests.
|
|
201
|
+
- **Rigor does not read tests for `rigor check`.** Its local
|
|
202
|
+
model leaves un-narrowed parameters at `Dynamic[top]`; the
|
|
203
|
+
bug-finding gate never consults `spec/` to tighten them.
|
|
204
|
+
Tests become a parameter signal only in the opt-in
|
|
205
|
+
`rigor sig-gen --params=observed --observe=spec/` path
|
|
206
|
+
([Chapter 11](11-sig-gen.md)), which unions the observed
|
|
207
|
+
argument type per position across every call site.
|
|
208
|
+
|
|
209
|
+
So the contrast is sharp: TypeProf interprets a spec as
|
|
210
|
+
ordinary Ruby (the `it` blocks are just more call sites),
|
|
211
|
+
whereas Rigor's sig-gen collector models the RSpec DSL
|
|
212
|
+
*structurally* — `described_class`, `subject`, and `let` are
|
|
213
|
+
recognised as bindings, not just executed:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
RSpec.describe Calc do
|
|
217
|
+
subject { Calc.new } # :subject → Nominal[Calc]
|
|
218
|
+
let(:other) { Calc.new } # :other → Nominal[Calc]
|
|
219
|
+
it { subject.greet("Alice") } # observed: Calc#greet receives String
|
|
220
|
+
it { described_class.new.add(1, 2) } # observed: Calc#add receives Integer, Integer
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Direction 2 — method types → tests
|
|
225
|
+
|
|
226
|
+
The reverse flow: once signatures land in `sig/`, they make
|
|
227
|
+
the *spec itself* checkable. `rigor check` (and the
|
|
228
|
+
`rigor-rspec` plugin) types `subject` / `let` bodies against
|
|
229
|
+
the real return types and checks matchers, which sharpens the
|
|
230
|
+
next `sig-gen` run, closing the loop. (Note the two RSpec
|
|
231
|
+
machines are separate: sig-gen's collector is built in and
|
|
232
|
+
needs no plugin; `rigor-rspec` is the standalone diagnostic
|
|
233
|
+
analyser. They run side by side, not shared.)
|
|
234
|
+
|
|
235
|
+
### The decisive difference — ADR-5
|
|
236
|
+
|
|
237
|
+
Observation-derived parameters are almost always **too
|
|
238
|
+
narrow**: a method only ever exercised with `String` in the
|
|
239
|
+
suite looks like it takes `(String)` even when it accepts far
|
|
240
|
+
more. The tools split on what to do about that.
|
|
241
|
+
|
|
242
|
+
- **TypeProf emits the observed-narrow parameter** — its job
|
|
243
|
+
is to report what the type-level run saw.
|
|
244
|
+
- **Rigor refuses to make that the default.** Under the
|
|
245
|
+
[robustness principle](../type-specification/robustness-principle.md)
|
|
246
|
+
(strict returns, lenient parameters), `--params=observed` is
|
|
247
|
+
a deliberate opt-in and its output is a *suggestion to
|
|
248
|
+
review*, not a frozen contract. The default `untyped` is the
|
|
249
|
+
stance that the suite's current usage should not silently
|
|
250
|
+
become every future caller's obligation.
|
|
251
|
+
|
|
252
|
+
| | TypeProf | Rigor |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| Role of tests | No special role — a test is just one more call site that fuels inference | Opt-in fuel for `sig-gen` only (never the `check` gate) |
|
|
255
|
+
| How a spec is read | Executed as ordinary Ruby (not recognised as a test) | `subject` / `let` / `described_class` recognised structurally |
|
|
256
|
+
| Observed-narrow params | Emitted as-is | Treated as a reviewable suggestion (ADR-5, opt-in, human gate) |
|
|
257
|
+
| Bidirectional loop | arg ↔ return within one pass | spec → sig → spec-checking → sharper sig |
|
|
258
|
+
|
|
259
|
+
Two honest limits on the Rigor side (both in Chapter 11):
|
|
260
|
+
same-name `let` bindings nested across `describe` / `context`
|
|
261
|
+
are last-wins, and `before` / `around` hook bodies are not
|
|
262
|
+
consulted for binding mutations.
|
|
263
|
+
|
|
264
|
+
## Diagnostics — a side effect vs the main product
|
|
265
|
+
|
|
266
|
+
TypeProf reports some errors as it interprets — an undefined
|
|
267
|
+
method, an obviously impossible operation — but these are a
|
|
268
|
+
*byproduct* of the inference pass, not a curated linter. They
|
|
269
|
+
can be noisy, and TypeProf is not pitched as a bug-finding gate.
|
|
270
|
+
|
|
271
|
+
Rigor inverts this. The diagnostic stream *is* the product,
|
|
272
|
+
and the governing rule (ADR-0, and the
|
|
273
|
+
[false-positive discipline](08-understanding-errors.md) the
|
|
274
|
+
whole tool is built around) is to stay silent unless the bug
|
|
275
|
+
is provable. Running `rigor check lib` yields a small,
|
|
276
|
+
high-confidence set of findings, not a transcript of
|
|
277
|
+
everything the interpreter found surprising.
|
|
278
|
+
|
|
279
|
+
So the mental shift coming from TypeProf is: do not expect
|
|
280
|
+
Rigor's output to be "the types it found" (use `sig-gen` for
|
|
281
|
+
that). Expect it to be "the small number of places the code is
|
|
282
|
+
provably wrong."
|
|
283
|
+
|
|
284
|
+
## Invocation
|
|
285
|
+
|
|
286
|
+
| TypeProf | Rigor |
|
|
287
|
+
| --- | --- |
|
|
288
|
+
| `typeprof app.rb` | `rigor sig-gen --print lib/app.rb` (for RBS) / `rigor check lib` (for bugs) |
|
|
289
|
+
| `typeprof -I sig app.rb` (load RBS) | `signature_paths:` in `.rigor.yml` (auto-detected) |
|
|
290
|
+
| `typeprof --lsp` (TypeProf for IDE) | `rigor lsp` (ADR-19) |
|
|
291
|
+
| harness file to drive call-site inference | not needed — local inference per method |
|
|
292
|
+
| core/stdlib RBS loaded automatically | catalog of core/stdlib types loaded automatically |
|
|
293
|
+
|
|
294
|
+
Both tools read gem RBS via the standard `rbs-collection`
|
|
295
|
+
mechanism, the same one Steep uses.
|
|
296
|
+
|
|
297
|
+
## What TypeProf has and Rigor does not
|
|
298
|
+
|
|
299
|
+
- **Parameter inference from call sites.** TypeProf's
|
|
300
|
+
whole-program interpretation lets it deduce a method's
|
|
301
|
+
*parameter* types from how the method is called. Rigor does
|
|
302
|
+
not — an un-annotated, un-narrowed parameter is
|
|
303
|
+
`Dynamic[top]` until a `.rbs` or a guard says otherwise.
|
|
304
|
+
- **True inter-procedural body interpretation.** TypeProf
|
|
305
|
+
re-interprets callee bodies; Rigor looks callees up in a
|
|
306
|
+
catalog. On a small self-contained program TypeProf can
|
|
307
|
+
follow data flow further than Rigor will.
|
|
308
|
+
- **First-class "infer the whole signature" output.** It is
|
|
309
|
+
TypeProf's primary product, with years of tuning aimed
|
|
310
|
+
squarely at it. `rigor sig-gen` is deliberately more
|
|
311
|
+
conservative (especially on parameters, per ADR-5).
|
|
312
|
+
|
|
313
|
+
## What Rigor has and TypeProf does not
|
|
314
|
+
|
|
315
|
+
- **Refinement carriers from value predicates.** Both tools do
|
|
316
|
+
flow-sensitive *occurrence typing* on type-identity predicates
|
|
317
|
+
(`is_a?`, `nil?`, `case`/`when`) — TypeProf included. What
|
|
318
|
+
Rigor adds is **refinement carriers**: a *value* predicate
|
|
319
|
+
like `unless s.empty?` or `n > 0` narrows into a named
|
|
320
|
+
refinement type (`non-empty-string`, `positive-int`). TypeProf
|
|
321
|
+
has no refinement-carrier concept, so those value-predicate
|
|
322
|
+
refinements widen back to `String` / `Integer`.
|
|
323
|
+
- **Constant folding through method calls.** `"foo".upcase`
|
|
324
|
+
resolves to `Constant<"FOO">`. TypeProf's output is
|
|
325
|
+
`String`.
|
|
326
|
+
- **A curated, false-positive-disciplined diagnostic gate.**
|
|
327
|
+
Rigor is a checker first; TypeProf's errors are a byproduct.
|
|
328
|
+
- **Whole-codebase scale with caching.** Inference budgets
|
|
329
|
+
plus a per-file cache (ADR-6) keep `rigor check` usable on a
|
|
330
|
+
large app on every run; TypeProf is positioned for smaller
|
|
331
|
+
inputs.
|
|
332
|
+
- **RBS::Extended directives.** `%a{rigor:v1:…}` refinement /
|
|
333
|
+
predicate / assertion grammar (Chapter 7) has no TypeProf
|
|
334
|
+
analogue.
|
|
335
|
+
- **The plugin ecosystem.** Sorbet-input adapter, Rails-side
|
|
336
|
+
narrowing, `dynamic_return` return-type
|
|
337
|
+
contributions — extension surfaces TypeProf does not model.
|
|
338
|
+
|
|
339
|
+
## A coexistence pattern
|
|
340
|
+
|
|
341
|
+
The two tools compose naturally because they sit at different
|
|
342
|
+
points in the lifecycle:
|
|
343
|
+
|
|
344
|
+
1. **Bootstrap with TypeProf (optional).** On a brand-new file
|
|
345
|
+
or small library with no sigs, `typeprof` can give you a
|
|
346
|
+
first RBS draft to read. (Or skip straight to step 2 —
|
|
347
|
+
Rigor does not need it.)
|
|
348
|
+
2. **Check with Rigor.** Run `rigor check lib` for the
|
|
349
|
+
provable-bug gate. This is the day-to-day signal.
|
|
350
|
+
3. **Generate sigs with `rigor sig-gen`** when you want RBS
|
|
351
|
+
that round-trips with Rigor's own inference and respects
|
|
352
|
+
the `tighter-return` no-overwrite rule.
|
|
353
|
+
4. **Add Rigor to CI.** The checker gate runs on every push;
|
|
354
|
+
TypeProf stays a local, occasional prototyping aid.
|
|
355
|
+
|
|
356
|
+
The standing rule when their RBS disagrees: TypeProf may infer
|
|
357
|
+
a *narrower parameter* from a call site that Rigor reports as
|
|
358
|
+
`untyped`. That is not a Rigor bug; it is the local-vs-whole-
|
|
359
|
+
program trade. If you want Rigor to honour that narrower
|
|
360
|
+
parameter, write it into `sig/` (both tools then read it) or
|
|
361
|
+
add a guard the engine can narrow on.
|
|
362
|
+
|
|
363
|
+
## A migration vignette
|
|
364
|
+
|
|
365
|
+
Suppose you have been using TypeProf to bootstrap `sig/` for a
|
|
366
|
+
mid-sized gem: you run `typeprof lib/**/*.rb`, hand-edit the
|
|
367
|
+
prototypes, and commit them. You want Rigor's bug-finding on
|
|
368
|
+
top.
|
|
369
|
+
|
|
370
|
+
Steps:
|
|
371
|
+
|
|
372
|
+
1. **Keep your generated `sig/`.** Rigor reads it as input
|
|
373
|
+
exactly like Steep would — TypeProf-authored RBS is just
|
|
374
|
+
RBS.
|
|
375
|
+
2. **Run `rigor check lib` once.** Expect a *different* kind
|
|
376
|
+
of output than TypeProf gave you: not signatures, but a
|
|
377
|
+
short list of provable findings — narrowing-aware
|
|
378
|
+
diagnostics (`flow.always-truthy-condition`), return
|
|
379
|
+
mismatches against your committed sigs
|
|
380
|
+
(`def.return-type-mismatch`). Triage as bugs vs noise.
|
|
381
|
+
3. **Switch your RBS-generation step to `rigor sig-gen`.**
|
|
382
|
+
Where you previously re-ran `typeprof` and re-edited, run
|
|
383
|
+
`rigor sig-gen --diff` instead. The classification model
|
|
384
|
+
(`new-file` / `new-method` / `tighter-return`) means it
|
|
385
|
+
will *not* clobber a parameter type you hand-tightened.
|
|
386
|
+
4. **Optionally tighten sigs with `RBS::Extended`.** TypeProf
|
|
387
|
+
treats `%a{rigor:v1:…}` as ordinary RBS comments and
|
|
388
|
+
ignores them; Rigor reads them as refinement directives.
|
|
389
|
+
The same `.rbs` file produces stricter Rigor output and
|
|
390
|
+
unchanged TypeProf output.
|
|
391
|
+
|
|
392
|
+
The migration is low-friction because the shared assumption —
|
|
393
|
+
RBS as the interchange format, inference as the default — is
|
|
394
|
+
exactly the one TypeProf trained you on.
|
|
395
|
+
|
|
396
|
+
## What's next
|
|
397
|
+
|
|
398
|
+
You probably do not need to read the rest of this appendix
|
|
399
|
+
section sequentially. Three useful pointers:
|
|
400
|
+
|
|
401
|
+
- [Chapter 11 — Generating RBS with `rigor sig-gen`](11-sig-gen.md)
|
|
402
|
+
— the feature that does TypeProf's job inside Rigor, with
|
|
403
|
+
the no-overwrite classification model.
|
|
404
|
+
- [Chapter 8 — Understanding errors](08-understanding-errors.md)
|
|
405
|
+
for the diagnostic gate that is Rigor's product, the thing
|
|
406
|
+
TypeProf does not set out to be.
|
|
407
|
+
- [`docs/type-specification/inference-budgets.md`](../type-specification/inference-budgets.md)
|
|
408
|
+
for the budget model that lets local inference scale where
|
|
409
|
+
whole-program interpretation does not.
|
|
410
|
+
|
|
411
|
+
If you want to compare against another tool, the sibling
|
|
412
|
+
appendix pages cover [Steep](appendix-steep.md) — Ruby's other
|
|
413
|
+
static checker — plus [TypeScript](appendix-typescript.md),
|
|
414
|
+
[PHPStan](appendix-phpstan.md), [mypy](appendix-mypy.md),
|
|
415
|
+
[Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
|
|
416
|
+
[Go](appendix-go.md), and [Elixir](appendix-elixir.md).
|