rigortype 0.2.1 → 0.2.3
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 +41 -14
- 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 +569 -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 +539 -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 +1 -1
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli/triage_command.rb +8 -2
- data/lib/rigor/cli/triage_renderer.rb +4 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +124 -32
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/triage/catalogue.rb +16 -1
- data/lib/rigor/triage.rb +30 -7
- 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 +90 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# Appendix — Coming from mypy / Pyright
|
|
2
|
+
|
|
3
|
+
If your static-typing baseline is Python's mypy or Pyright,
|
|
4
|
+
this appendix maps the vocabulary onto Rigor's. The two
|
|
5
|
+
ecosystems share more than they look like at first (gradual
|
|
6
|
+
typing, a "do not break the runtime" philosophy, separate
|
|
7
|
+
type-stub files `.pyi` / `.rbs`), but they make different
|
|
8
|
+
choices about where annotations live and how aggressive
|
|
9
|
+
inference is.
|
|
10
|
+
|
|
11
|
+
## The five-second pitch
|
|
12
|
+
|
|
13
|
+
| Question | mypy / Pyright | Rigor |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Where do annotations live? | In source (`def f(x: int) -> str:`) | In `.rbs` files alongside `.rb` |
|
|
16
|
+
| Stub format | `.pyi` files | `.rbs` files |
|
|
17
|
+
| Default for unannotated code | `Any` everywhere (mypy) / inferred (Pyright) | Inferred precisely or `Dynamic[top]` |
|
|
18
|
+
| Strict mode | `--strict` (mypy) / `strict: true` (Pyright) | `severity_profile: strict` |
|
|
19
|
+
| Suppression | `# type: ignore[error-code]` | `# rigor:disable <rule>` |
|
|
20
|
+
| Identity of types | Nominal + structural (Protocol) | Nominal + structural facets |
|
|
21
|
+
| Narrowing | Flow-sensitive, type guards | Flow-sensitive, predicate methods + RBS::Extended |
|
|
22
|
+
|
|
23
|
+
The Python / Ruby parallels run deeper than syntax: both
|
|
24
|
+
languages were born dynamic, both grew gradual typing
|
|
25
|
+
late, both treat type-checking as advisory, and both ship
|
|
26
|
+
official syntax for type hints (Python's `typing`, Ruby's
|
|
27
|
+
`RBS`). Many of Rigor's design priorities echo the things
|
|
28
|
+
mypy got right.
|
|
29
|
+
|
|
30
|
+
## Type vocabulary mapping
|
|
31
|
+
|
|
32
|
+
| Python typing | Rigor representation | Notes |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `int` | `Integer` | |
|
|
35
|
+
| `float` | `Float` | |
|
|
36
|
+
| `bool` | `bool` (`Constant<true> \| Constant<false>`) | |
|
|
37
|
+
| `str` | `String` | |
|
|
38
|
+
| `bytes` | `String` (with binary encoding) | Ruby has no separate `bytes` type. |
|
|
39
|
+
| `None` | `Constant<nil>` | `nil` is Ruby's sole no-value. |
|
|
40
|
+
| `Any` | `Dynamic[top]` | "Stay silent" carrier. |
|
|
41
|
+
| `object` | `Object` (or `Top`) | `object` is Python's universal supertype (everything, `None` included); Rigor's nearest match is `Top`. |
|
|
42
|
+
| `Never` / `NoReturn` | `Bot` | Empty type. |
|
|
43
|
+
| `Optional[T]` / `T \| None` | `T?` (i.e., `T \| nil`) | |
|
|
44
|
+
| `Union[A, B]` / `A \| B` | `A \| B` | Same display. |
|
|
45
|
+
| `Literal[42]` | `Constant<42>` | Direct match. |
|
|
46
|
+
| `Literal["foo", "bar"]` | `Constant<"foo"> \| Constant<"bar">` | |
|
|
47
|
+
| `Final[T]` | (no analogue) | Rigor does not yet track immutability. |
|
|
48
|
+
| `tuple[int, str]` | `Tuple[Integer, String]` | Same per-position model. |
|
|
49
|
+
| `list[T]` | `Array[T]` | |
|
|
50
|
+
| `dict[K, V]` | `Hash[K, V]` | |
|
|
51
|
+
| `set[T]` | `Set[T]` | |
|
|
52
|
+
| `TypedDict` | `HashShape{...}` | Closed shape with required / optional keys. |
|
|
53
|
+
| `NotRequired[T]` (TypedDict) | optional key in `HashShape` | Covered by Rigor's per-key required/optional flag. |
|
|
54
|
+
| `Callable[[int], str]` | `^(Integer) -> String` (RBS proc/block syntax) | |
|
|
55
|
+
| `TypeVar('T')` | RBS `[T]` type parameter | |
|
|
56
|
+
| `Generic[T]` | RBS `class Foo[T]` | |
|
|
57
|
+
| `Protocol` (PEP 544) | RBS `interface _Foo` | Structural typing. |
|
|
58
|
+
| `runtime_checkable` Protocol | (no analogue) | Rigor does not run `isinstance` against structural protocols. |
|
|
59
|
+
| `Self` (PEP 673) | RBS `self` type | |
|
|
60
|
+
| `ClassVar[T]` | `attr_*` on the singleton side / `self.@var` | |
|
|
61
|
+
| `Annotated[T, "tag"]` | `RBS::Extended` `%a{...}` annotation | Both attach metadata to a type. |
|
|
62
|
+
|
|
63
|
+
## Refinement carriers vs Python annotation idioms
|
|
64
|
+
|
|
65
|
+
Python's typing system has been adding refinement-shaped
|
|
66
|
+
features one at a time (`Literal`, `LiteralString`, `TypeIs`,
|
|
67
|
+
`Annotated`). Rigor ships a broader catalogue out of the box.
|
|
68
|
+
|
|
69
|
+
| Rigor refinement | Python closest |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `non-empty-string` | (no built-in; PEP 675's `LiteralString` is closest in spirit but different in semantics) |
|
|
72
|
+
| `literal-string` | `LiteralString` (PEP 675) — provably built from source-code literals. **Direct match.** |
|
|
73
|
+
| `positive-int` | (no built-in; convention is `Annotated[int, Gt(0)]` with third-party validators) |
|
|
74
|
+
| `int<min, max>` | (no built-in; same `Annotated[int, Range(...)]` convention) |
|
|
75
|
+
| `numeric-string` | (no built-in) |
|
|
76
|
+
| `non-empty-array[T]` | (no built-in; some libraries use `tuple[T, *tuple[T, ...]]`) |
|
|
77
|
+
| `Constant<42>` | `Literal[42]` |
|
|
78
|
+
|
|
79
|
+
`LiteralString` is the deepest equivalence — both
|
|
80
|
+
Python's `LiteralString` and Rigor's `literal-string` carry the
|
|
81
|
+
"this string came from source code, not from runtime input"
|
|
82
|
+
fact, and both compose through formatting / interpolation.
|
|
83
|
+
|
|
84
|
+
## Narrowing — the part that feels familiar
|
|
85
|
+
|
|
86
|
+
Both checkers are flow-sensitive. The narrowing primitives
|
|
87
|
+
have direct analogues:
|
|
88
|
+
|
|
89
|
+
| Python | Rigor |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `if x:` | `if x` — strips `False` / `None` from truthy edge |
|
|
92
|
+
| `if x is None:` | `if x.nil?` |
|
|
93
|
+
| `if x is not None:` | `unless x.nil?` |
|
|
94
|
+
| `isinstance(x, int)` | `x.is_a?(Integer)` |
|
|
95
|
+
| `if isinstance(x, (int, str)):` | `if x.is_a?(Integer) \|\| x.is_a?(String)` |
|
|
96
|
+
| `assert isinstance(x, T)` | `# rigor:assert-type` style via plugin OR `T.cast` via `rigor-sorbet` |
|
|
97
|
+
| `match x: case ...` (PEP 634) | `case x; in ...` (Ruby's pattern matching) |
|
|
98
|
+
| User-defined `TypeGuard[T]` (PEP 647) | `%a{rigor:v1:predicate-if-true x is T}` directive |
|
|
99
|
+
| User-defined `TypeIs[T]` (PEP 742) | Same directive — Rigor's narrowing is symmetric (truthy AND falsey) by default |
|
|
100
|
+
| `assert x is not None; x.upper()` | Same idiom: `unless x.nil?; x.upcase; end` |
|
|
101
|
+
| `cast(int, x)` | `T.cast(x, Integer)` via `rigor-sorbet`, or RBS-side `param:` directive |
|
|
102
|
+
|
|
103
|
+
Notable: Python's `TypeGuard` is one-sided (narrows only the
|
|
104
|
+
truthy edge), while `TypeIs` (PEP 742, accepted) is two-sided.
|
|
105
|
+
Rigor's `predicate-if-true` and `predicate-if-false` directives
|
|
106
|
+
are independent and compose — by default declaring
|
|
107
|
+
`predicate-if-true: x is T` also narrows the falsey edge to
|
|
108
|
+
`x is ~T`, equivalent to `TypeIs`.
|
|
109
|
+
|
|
110
|
+
## Stubs ↔ RBS
|
|
111
|
+
|
|
112
|
+
Python's `.pyi` files and Rigor's `.rbs` files play the same
|
|
113
|
+
role: declare types for a library that does not ship them
|
|
114
|
+
inline.
|
|
115
|
+
|
|
116
|
+
| Python | Rigor |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `.pyi` stubs | `.rbs` files |
|
|
119
|
+
| `typeshed` (community-maintained stubs) | `rbs_collection` + Rigor's bundled stdlib catalogues |
|
|
120
|
+
| `mypy_path` config | `signature_paths:` in `.rigor.yml` |
|
|
121
|
+
| `py.typed` marker | (no analogue — Rigor checks any file under `paths:`) |
|
|
122
|
+
| `from __future__ import annotations` | (no analogue — RBS is always lazy by virtue of file separation) |
|
|
123
|
+
| Reveal type: `reveal_type(x)` | `dump_type(x)` (info diagnostic) / `assert_type("...", x)` |
|
|
124
|
+
|
|
125
|
+
`reveal_type` and `dump_type` are the same tool with different
|
|
126
|
+
names — both emit the inferred type at the call site as a
|
|
127
|
+
diagnostic, both are no-ops at runtime in idiomatic test
|
|
128
|
+
harnesses, both are the canonical "what does the checker see
|
|
129
|
+
here?" probe.
|
|
130
|
+
|
|
131
|
+
## Severity, suppression, and "strict mode"
|
|
132
|
+
|
|
133
|
+
| Python (mypy) | Rigor |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `--strict` | `severity_profile: strict` |
|
|
136
|
+
| `--strict-optional` | Always-on in Rigor (no separate flag) |
|
|
137
|
+
| `--no-implicit-optional` | Always-on in Rigor |
|
|
138
|
+
| `--check-untyped-defs` | Always-on in Rigor |
|
|
139
|
+
| `--disallow-untyped-defs` | (no analogue — Rigor never demands annotations) |
|
|
140
|
+
| `--disallow-any-explicit` | (no analogue) |
|
|
141
|
+
| `# type: ignore` | `# rigor:disable all` |
|
|
142
|
+
| `# type: ignore[error-code]` | `# rigor:disable <rule>` |
|
|
143
|
+
| `# mypy: ignore-errors` (file scope) | `# rigor:disable-file all` |
|
|
144
|
+
| `mypy.ini` / `pyproject.toml` | `.rigor.yml` / `.rigor.dist.yml` |
|
|
145
|
+
|
|
146
|
+
The conceptual gap: mypy's `--disallow-untyped-defs` reflects
|
|
147
|
+
its baseline assumption that annotations should exist
|
|
148
|
+
everywhere. Rigor never demands annotations — inference is
|
|
149
|
+
always the first answer, RBS is the escape hatch. That makes
|
|
150
|
+
adoption smoother: there is no "you must annotate this whole
|
|
151
|
+
module before mypy is useful" stage.
|
|
152
|
+
|
|
153
|
+
## Pyright vs Rigor
|
|
154
|
+
|
|
155
|
+
Pyright (Microsoft's type checker, the engine behind Pylance)
|
|
156
|
+
is closer to Rigor in spirit than mypy is — both prioritise
|
|
157
|
+
inference depth and pragmatic narrowing over annotation
|
|
158
|
+
completeness.
|
|
159
|
+
|
|
160
|
+
| Pyright | Rigor |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `# pyright: ignore[reportError]` | `# rigor:disable <rule>` |
|
|
163
|
+
| `pyright --stats` | (no direct analogue — `rigor check --explain` surfaces gradual fallback decisions) |
|
|
164
|
+
| Inferred return types from body | Same — `def` bodies are walked and the inferred return propagates |
|
|
165
|
+
| Speculative inference (Pyright is fast) | Rigor's type-objects are immutable shared structures; cache-driven recompute is incremental |
|
|
166
|
+
| Strict / basic / off file-level setting | `severity_profile:` is project-wide; per-file via `# rigor:disable-file` |
|
|
167
|
+
|
|
168
|
+
If you have used Pyright's "infer aggressively, then narrow"
|
|
169
|
+
authoring loop, Rigor will feel familiar. The biggest
|
|
170
|
+
adjustment is that Rigor's annotations live in `.rbs` files,
|
|
171
|
+
not in the `.rb` source.
|
|
172
|
+
|
|
173
|
+
## "No annotations needed" — true here too
|
|
174
|
+
|
|
175
|
+
Take a canonical mypy onboarding example:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
def classify(n: int) -> Literal["zero", "positive", "negative"]:
|
|
179
|
+
if n == 0:
|
|
180
|
+
return "zero"
|
|
181
|
+
if n > 0:
|
|
182
|
+
return "positive"
|
|
183
|
+
return "negative"
|
|
184
|
+
|
|
185
|
+
result = classify(7)
|
|
186
|
+
# mypy: result: Literal['zero', 'positive', 'negative']
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The Rigor equivalent — no annotations:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
def classify(n)
|
|
193
|
+
return :zero if n.zero?
|
|
194
|
+
return :positive if n.positive?
|
|
195
|
+
:negative
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
result = classify(7)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Same precision; one writes the parameter and return
|
|
202
|
+
annotation, the other does not.
|
|
203
|
+
|
|
204
|
+
When you need a sig — for a public library boundary, for
|
|
205
|
+
parameter validation, for `def.return-type-mismatch` to fire —
|
|
206
|
+
that goes into `sig/<file>.rbs`, not into the `.rb` source.
|
|
207
|
+
|
|
208
|
+
## Generics
|
|
209
|
+
|
|
210
|
+
Both ecosystems have generics; Rigor's are RBS's.
|
|
211
|
+
|
|
212
|
+
| Python | Rigor (via RBS) |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `T = TypeVar('T')` | `[T]` after the method or class name |
|
|
215
|
+
| `def first(xs: list[T]) -> T` | `def first: [T] (Array[T]) -> T` |
|
|
216
|
+
| `Generic[T]` class | `class Foo[T]` |
|
|
217
|
+
| `T = TypeVar('T', bound=Comparable)` | `[T < Comparable]` (RBS bounded type parameters) |
|
|
218
|
+
| `ParamSpec` | (no analogue today) |
|
|
219
|
+
| `TypeVarTuple` | (no analogue today) |
|
|
220
|
+
| `Concatenate[X, P]` | (no analogue today) |
|
|
221
|
+
|
|
222
|
+
Rigor's generics coverage matches RBS's — it is more
|
|
223
|
+
conservative than Python's `typing` ecosystem, but covers the
|
|
224
|
+
common cases (collections, methods over generic containers,
|
|
225
|
+
class-level type parameters).
|
|
226
|
+
|
|
227
|
+
## Protocols ↔ RBS interfaces
|
|
228
|
+
|
|
229
|
+
Python's PEP 544 introduced structural typing via `Protocol`.
|
|
230
|
+
Ruby's RBS has had structural `interface _Foo` since its first
|
|
231
|
+
release.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
class SupportsClose(Protocol):
|
|
235
|
+
def close(self) -> None: ...
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
```rbs
|
|
239
|
+
interface _SupportsClose
|
|
240
|
+
def close: () -> void
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A class that defines `close` (with the right signature)
|
|
245
|
+
satisfies both. Neither system requires the class to declare
|
|
246
|
+
inheritance — the structural match is implicit.
|
|
247
|
+
|
|
248
|
+
Rigor reads RBS interfaces from `sig/`. When an RBS-declared
|
|
249
|
+
parameter is `_SupportsClose`, Rigor checks the call site's
|
|
250
|
+
argument structurally, the same way mypy / Pyright check
|
|
251
|
+
against a `Protocol`.
|
|
252
|
+
|
|
253
|
+
One word of warning carried over from Python: in Rigor "protocol"
|
|
254
|
+
does **not** mean this. The structural-type concept is the RBS
|
|
255
|
+
`interface`; "protocol" is reserved for a different,
|
|
256
|
+
plugin-declared feature (path-scoped behavioural contracts). The
|
|
257
|
+
[Protocols and structural typing appendix](appendix-protocols-and-structural-typing.md)
|
|
258
|
+
draws the distinction in full.
|
|
259
|
+
|
|
260
|
+
## What mypy / Pyright have and Rigor does not
|
|
261
|
+
|
|
262
|
+
- **Variance annotations on TypeVars.** `TypeVar('T',
|
|
263
|
+
covariant=True)`. Rigor relies on RBS's variance, which is
|
|
264
|
+
fixed per the standard library — there is no user-side
|
|
265
|
+
variance authoring.
|
|
266
|
+
- **`Final` / immutability tracking.** Rigor does not yet
|
|
267
|
+
model "this name is never reassigned."
|
|
268
|
+
- **`@overload` stacks.** RBS supports method overloads, but
|
|
269
|
+
the dispatch logic in Rigor's analyzer is more conservative
|
|
270
|
+
than mypy's pattern-based overload resolution.
|
|
271
|
+
- **Decorator-aware type transformation.** Python's typing
|
|
272
|
+
ecosystem has well-developed support for decorators that
|
|
273
|
+
transform a function's type. Ruby's analogue is less common,
|
|
274
|
+
and Rigor does not yet model `Module#prepend` /
|
|
275
|
+
`define_method` transformations.
|
|
276
|
+
- **`async` / `await` types.** Ruby has Fiber and Async, but
|
|
277
|
+
the RBS surface for async types is patchier than Python's
|
|
278
|
+
`Coroutine[T, U, V]`.
|
|
279
|
+
|
|
280
|
+
## What Rigor has and mypy / Pyright do not
|
|
281
|
+
|
|
282
|
+
- **Constant folding through method calls.** mypy and Pyright
|
|
283
|
+
both fold literals, but neither folds through arbitrary
|
|
284
|
+
built-in methods. Rigor folds through a catalogued set of
|
|
285
|
+
pure methods on `Numeric`, `String`, `Symbol`, `Array`,
|
|
286
|
+
`Hash`.
|
|
287
|
+
- **First-class refinement carriers with narrowing.**
|
|
288
|
+
`non-empty-string`, `positive-int`, `numeric-string`,
|
|
289
|
+
`int<min, max>` — values restricted by predicate, narrowed
|
|
290
|
+
by the corresponding Ruby predicate methods.
|
|
291
|
+
- **No-false-positives stance.** mypy will warn about dynamic
|
|
292
|
+
code unless `--no-warn-unused-ignores` or `--ignore-missing-imports`
|
|
293
|
+
is set; Rigor stays silent on `Dynamic[top]` without
|
|
294
|
+
configuration.
|
|
295
|
+
- **Plugin-side return-type variation by argument shape.**
|
|
296
|
+
Pyright's "type alias narrowing" and mypy's overload stacks
|
|
297
|
+
cover some cases; Rigor's plugin contract gives you full
|
|
298
|
+
Ruby code at the dispatch point. The
|
|
299
|
+
[`rigor-lisp-eval`](../../examples/rigor-lisp-eval/) example
|
|
300
|
+
is the canonical demo — `Lisp.eval([:+, 1, 2])` returns
|
|
301
|
+
`Integer`, `Lisp.eval([:<, 1, 2])` returns `bool`.
|
|
302
|
+
|
|
303
|
+
## A migration vignette
|
|
304
|
+
|
|
305
|
+
You are porting a mypy-tightened Python module to Ruby. The
|
|
306
|
+
original:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
def classify_input(s: str) -> Literal["empty", "numeric", "text"]:
|
|
310
|
+
if not s:
|
|
311
|
+
return "empty"
|
|
312
|
+
if s.isdigit():
|
|
313
|
+
return "numeric"
|
|
314
|
+
return "text"
|
|
315
|
+
|
|
316
|
+
def shout(s: str) -> str:
|
|
317
|
+
assert s, "expected non-empty"
|
|
318
|
+
return s.upper()
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The Rigor port:
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# lib/text_utils.rb
|
|
325
|
+
def classify_input(s)
|
|
326
|
+
return :empty if s.empty?
|
|
327
|
+
return :numeric if s.match?(/\A\d+\z/)
|
|
328
|
+
:text
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def shout(s)
|
|
332
|
+
raise ArgumentError if s.empty?
|
|
333
|
+
s.upcase
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```rbs
|
|
338
|
+
# sig/text_utils.rbs
|
|
339
|
+
%a{rigor:v1:return: Constant<:empty> | Constant<:numeric> | Constant<:text>}
|
|
340
|
+
def classify_input: (String s) -> Symbol
|
|
341
|
+
|
|
342
|
+
%a{rigor:v1:param: s is non-empty-string}
|
|
343
|
+
def shout: (String s) -> non-empty-string
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
You gain: `s.empty?` is a recognised refinement narrower (no
|
|
347
|
+
need for `assert s`). `match?(/\A\d+\z/)` does not yet narrow
|
|
348
|
+
to `numeric-string` (this is on the v0.1.1 roadmap — see
|
|
349
|
+
[`docs/ROADMAP.md`](../ROADMAP.md)), but the eventual
|
|
350
|
+
behaviour will mirror `s.isdigit()` narrowing in Pyright.
|
|
351
|
+
|
|
352
|
+
## What's next
|
|
353
|
+
|
|
354
|
+
You probably do not need to read the rest of this appendix
|
|
355
|
+
section sequentially. Three useful pointers:
|
|
356
|
+
|
|
357
|
+
- [Chapter 2 — Everyday types](02-everyday-types.md) for the
|
|
358
|
+
carrier zoo if the refinement vocabulary is new.
|
|
359
|
+
- [Chapter 3 — Narrowing](03-narrowing.md) for the
|
|
360
|
+
flow-sensitive rules — direct analogues to mypy's narrowing.
|
|
361
|
+
- [Chapter 7 — RBS and `RBS::Extended`](07-rbs-and-extended.md)
|
|
362
|
+
for the directive grammar — `predicate-if-true` is Rigor's
|
|
363
|
+
`TypeGuard` / `TypeIs`.
|
|
364
|
+
|
|
365
|
+
If you want to compare against another tool, the sibling
|
|
366
|
+
appendix pages cover [TypeScript](appendix-typescript.md),
|
|
367
|
+
[PHPStan](appendix-phpstan.md), [Steep](appendix-steep.md),
|
|
368
|
+
[TypeProf](appendix-typeprof.md),
|
|
369
|
+
[Java / C#](appendix-java-csharp.md), [Rust](appendix-rust.md),
|
|
370
|
+
[Go](appendix-go.md), and [Elixir](appendix-elixir.md).
|