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,427 @@
|
|
|
1
|
+
# RBS and RBS::Extended
|
|
2
|
+
|
|
3
|
+
When Rigor's inference cannot prove a type, the next escape
|
|
4
|
+
hatch is RBS — Ruby's signature language. When RBS cannot
|
|
5
|
+
express the precise contract you want, `RBS::Extended` adds a
|
|
6
|
+
small annotation surface on top.
|
|
7
|
+
|
|
8
|
+
This chapter covers both, in the order you usually reach for
|
|
9
|
+
them.
|
|
10
|
+
|
|
11
|
+
## When you need RBS
|
|
12
|
+
|
|
13
|
+
You probably need to add an RBS file when:
|
|
14
|
+
|
|
15
|
+
- The method body's return type depends on an external gem
|
|
16
|
+
Rigor's bundled stdlib does not cover.
|
|
17
|
+
- You want `call.argument-type-mismatch` to fire on
|
|
18
|
+
argument-shape errors (in-source `def` does NOT enforce
|
|
19
|
+
parameter contracts; only RBS-declared methods do).
|
|
20
|
+
- You want `def.return-type-mismatch` to fire when a body's
|
|
21
|
+
inferred return drifts from the declared return.
|
|
22
|
+
- A future RBS-aware tool (Steep, ruby-lsp) will read the
|
|
23
|
+
same file and benefit from the contract.
|
|
24
|
+
|
|
25
|
+
You probably do **not** need RBS when:
|
|
26
|
+
|
|
27
|
+
- The method is private to your project, the body is short,
|
|
28
|
+
and Rigor already infers the right return type.
|
|
29
|
+
- The method is a wrapper around a method that already
|
|
30
|
+
has a sig (Rigor walks the body and propagates).
|
|
31
|
+
|
|
32
|
+
## A first sig
|
|
33
|
+
|
|
34
|
+
In a fresh project:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
my-app/
|
|
38
|
+
├── lib/
|
|
39
|
+
│ └── slug.rb
|
|
40
|
+
└── sig/
|
|
41
|
+
└── slug.rbs # ← your sig
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# lib/slug.rb
|
|
46
|
+
class Slug
|
|
47
|
+
def normalise(id)
|
|
48
|
+
id.downcase.gsub(/\s+/, "-")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```rbs
|
|
54
|
+
# sig/slug.rbs
|
|
55
|
+
class Slug
|
|
56
|
+
def normalise: (String) -> String
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Drop the `.rbs` file in `sig/` and Rigor picks it up
|
|
61
|
+
automatically — no `.rigor.yml` change required. The default
|
|
62
|
+
config has `signature_paths: [sig]`.
|
|
63
|
+
|
|
64
|
+
After that, this code:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Slug.new.normalise(42)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
fires `call.argument-type-mismatch`: `42` is an Integer, the
|
|
71
|
+
parameter is `String`.
|
|
72
|
+
|
|
73
|
+
## When the RBS shape is too wide
|
|
74
|
+
|
|
75
|
+
The Slug example's runtime always returns a non-empty,
|
|
76
|
+
lowercase string — but the RBS sig only says `String`. If you
|
|
77
|
+
want Rigor to know the narrower fact, attach an `RBS::Extended`
|
|
78
|
+
annotation:
|
|
79
|
+
|
|
80
|
+
```rbs
|
|
81
|
+
class Slug
|
|
82
|
+
%a{rigor:v1:return: non-empty-lowercase-string}
|
|
83
|
+
def normalise: (String) -> String
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Now:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
s = Slug.new.normalise("Hello World")
|
|
91
|
+
# s: non-empty-lowercase-string
|
|
92
|
+
s.empty? # Constant<false> — proven
|
|
93
|
+
s.size # positive-int — proven
|
|
94
|
+
s == "hello-world" # bool — equality narrowing applies
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `.rbs` file is **still valid RBS** — `%a{...}` is the RBS
|
|
98
|
+
annotation syntax. Steep / typeprof / ruby-lsp see a comment;
|
|
99
|
+
Rigor sees a tightening.
|
|
100
|
+
|
|
101
|
+
## The directive grammar
|
|
102
|
+
|
|
103
|
+
`RBS::Extended` lives at
|
|
104
|
+
[`docs/type-specification/rbs-extended.md`](../type-specification/rbs-extended.md).
|
|
105
|
+
The per-method directives:
|
|
106
|
+
|
|
107
|
+
| Directive | Says |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `%a{rigor:v1:return: <type>}` | Tighten the method's return type. |
|
|
110
|
+
| `%a{rigor:v1:param: <name> is <type>}` | Tighten a parameter's accepted type at the call site, AND narrow the local in the body. |
|
|
111
|
+
| `%a{rigor:v1:assert <name> is <type>}` | After this method returns, the named local in the caller's scope is `<type>`. |
|
|
112
|
+
| `%a{rigor:v1:predicate-if-true <name> is <type>}` | When this method returns truthy, the named local in the caller's scope is `<type>`. (Symmetric `predicate-if-false`.) |
|
|
113
|
+
| `%a{rigor:v1:assert-if-true <name> is <type>}` | When this method returns a truthy value, the named local in the caller's scope is `<type>`. (Symmetric `assert-if-false` for `false` / `nil` returns.) |
|
|
114
|
+
|
|
115
|
+
The `<type>` slot accepts:
|
|
116
|
+
|
|
117
|
+
- **RBS class names** — `String`, `Integer`, `::Foo::Bar`.
|
|
118
|
+
- **Imported refinement names** —
|
|
119
|
+
`non-empty-string`, `lowercase-string`, `numeric-string`,
|
|
120
|
+
`int<5, 10>`, `non-empty-array[Integer]`, `literal-string`,
|
|
121
|
+
…
|
|
122
|
+
- **Negation `~T`** — `~lowercase-string` means
|
|
123
|
+
"non-lowercase-string."
|
|
124
|
+
|
|
125
|
+
## Refinement names
|
|
126
|
+
|
|
127
|
+
The full catalogue is in
|
|
128
|
+
[`docs/type-specification/imported-built-in-types.md`](../type-specification/imported-built-in-types.md).
|
|
129
|
+
A short reference:
|
|
130
|
+
|
|
131
|
+
| Family | Names |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| Empty / non-empty | `non-empty-string`, `non-empty-array[T]`, `non-empty-hash[K, V]` |
|
|
134
|
+
| Integer ranges | `positive-int`, `non-negative-int`, `negative-int`, `non-positive-int`, `non-zero-int`, `int<min, max>` |
|
|
135
|
+
| String predicates | `lowercase-string`, `uppercase-string`, `numeric-string`, `decimal-int-string`, `octal-int-string`, `hex-int-string`, `literal-string` |
|
|
136
|
+
| Paired complements | `non-lowercase-string`, `non-uppercase-string`, `non-numeric-string` |
|
|
137
|
+
| Composed | `non-empty-lowercase-string`, `non-empty-uppercase-string`, `non-empty-literal-string` |
|
|
138
|
+
| Shape projections | `pick_of[T, K]`, `omit_of[T, K]`, `partial_of[T]`, `required_of[T]`, `readonly_of[T]` — derive new `HashShape` / `Tuple` carriers from existing ones. See [chapter 4 § "Deriving new shapes"](04-tuples-and-shapes.md#deriving-new-shapes--pick_of--omit_of--partial_of--required_of--readonly_of). |
|
|
139
|
+
|
|
140
|
+
## Declaring conformance — `conforms-to`
|
|
141
|
+
|
|
142
|
+
The directives above attach to a `def`. One more attaches to a
|
|
143
|
+
`class` / `module` declaration and asserts the whole class
|
|
144
|
+
satisfies a named structural interface, as a checked design
|
|
145
|
+
assertion — whether or not any call site exercises it:
|
|
146
|
+
|
|
147
|
+
```rbs
|
|
148
|
+
%a{rigor:v1:conforms-to _RewindableStream}
|
|
149
|
+
class MyBuffer
|
|
150
|
+
def read: (Integer) -> String
|
|
151
|
+
def rewind: () -> void
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
If `MyBuffer` is missing a method the `_RewindableStream`
|
|
156
|
+
interface requires (or the signature is incompatible), Rigor
|
|
157
|
+
reports `rbs_extended.unsatisfied-conformance`; a class that
|
|
158
|
+
satisfies the interface is silent. Multiple `conforms-to`
|
|
159
|
+
directives on one class combine like an intersection of
|
|
160
|
+
interfaces. The directive is purely additive — implicit
|
|
161
|
+
structural compatibility at call sites keeps working with or
|
|
162
|
+
without it.
|
|
163
|
+
|
|
164
|
+
## Worked example: an assertion gate
|
|
165
|
+
|
|
166
|
+
```rbs
|
|
167
|
+
class Validator
|
|
168
|
+
%a{rigor:v1:assert x is non-empty-string}
|
|
169
|
+
def assert_non_empty: (String x) -> void
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
def configure(host)
|
|
175
|
+
Validator.new.assert_non_empty(host)
|
|
176
|
+
# host: non-empty-string after this call
|
|
177
|
+
host.size # positive-int — proven
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The runtime side is whatever `assert_non_empty` does (raise
|
|
182
|
+
on empty, log, ...) — Rigor only reads the directive.
|
|
183
|
+
|
|
184
|
+
## Worked example: a type predicate
|
|
185
|
+
|
|
186
|
+
```rbs
|
|
187
|
+
class Range
|
|
188
|
+
%a{rigor:v1:predicate-if-true value is Integer}
|
|
189
|
+
def integer?: (untyped value) -> bool
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
def double_if_int(value)
|
|
195
|
+
if (1..10).integer?(value)
|
|
196
|
+
# value: Integer in the truthy branch
|
|
197
|
+
value * 2
|
|
198
|
+
else
|
|
199
|
+
value
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This is the supported way to teach Rigor about a custom
|
|
205
|
+
type-predicate method that the engine's built-in `is_a?` /
|
|
206
|
+
`nil?` rules cannot recognise.
|
|
207
|
+
|
|
208
|
+
## Worked example: parameter override
|
|
209
|
+
|
|
210
|
+
```rbs
|
|
211
|
+
class Slug
|
|
212
|
+
%a{rigor:v1:param: id is non-empty-string}
|
|
213
|
+
def normalise: (String id) -> String
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This has two effects:
|
|
218
|
+
|
|
219
|
+
1. **Call-site checking.** `Slug.new.normalise("")` is now a
|
|
220
|
+
`call.argument-type-mismatch` because `Constant<"">` does
|
|
221
|
+
not satisfy `non-empty-string`.
|
|
222
|
+
2. **Body-side narrowing.** Inside the method body of
|
|
223
|
+
`normalise`, the parameter `id` is `non-empty-string`. So
|
|
224
|
+
`id.empty?` reduces to `Constant<false>` and `id.size`
|
|
225
|
+
reduces to `positive-int`.
|
|
226
|
+
|
|
227
|
+
## When you need a parameter override the runtime cannot enforce
|
|
228
|
+
|
|
229
|
+
Sometimes the runtime function does NOT raise on bad input —
|
|
230
|
+
it returns nil, returns a default, or swallows the error.
|
|
231
|
+
Rigor's `param:` directive still tightens the call-site
|
|
232
|
+
contract:
|
|
233
|
+
|
|
234
|
+
```rbs
|
|
235
|
+
class FileLoader
|
|
236
|
+
%a{rigor:v1:param: path is non-empty-string}
|
|
237
|
+
def load: (String path) -> String?
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`FileLoader.new.load("")` fires `call.argument-type-mismatch`
|
|
242
|
+
even though at runtime `load` would fail gracefully. The
|
|
243
|
+
directive expresses **what callers should pass**, not what
|
|
244
|
+
the body enforces.
|
|
245
|
+
|
|
246
|
+
## Where annotations belong
|
|
247
|
+
|
|
248
|
+
Put `RBS::Extended` annotations on the same `def` they refine,
|
|
249
|
+
inside the same `.rbs` file. Group them above the method:
|
|
250
|
+
|
|
251
|
+
```rbs
|
|
252
|
+
class Slug
|
|
253
|
+
%a{rigor:v1:return: non-empty-string}
|
|
254
|
+
%a{rigor:v1:param: id is non-empty-string}
|
|
255
|
+
def normalise: (String id) -> String
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
You **cannot** put these `%a{rigor:v1:…}` directives inside a
|
|
260
|
+
`.rb` file. The directives only fire when read from RBS —
|
|
261
|
+
that is a design choice (see
|
|
262
|
+
ADR-5, the robustness principle: strict on returns, lenient
|
|
263
|
+
on parameters).
|
|
264
|
+
|
|
265
|
+
## Inline RBS in Ruby source — the `rigor-rbs-inline` plugin
|
|
266
|
+
|
|
267
|
+
A separate, opt-in plugin lets you write method types directly
|
|
268
|
+
above the `def` in your Ruby file, using the
|
|
269
|
+
[rbs-inline](https://github.com/soutaro/rbs-inline) comment
|
|
270
|
+
vocabulary upstream defines:
|
|
271
|
+
|
|
272
|
+
```rb
|
|
273
|
+
# rbs_inline: enabled
|
|
274
|
+
|
|
275
|
+
class AscDesc
|
|
276
|
+
# @rbs asc_or_desc: :asc | :desc
|
|
277
|
+
def ascdesc(asc_or_desc)
|
|
278
|
+
asc_or_desc
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
AscDesc.new.ascdesc(:bad)
|
|
283
|
+
# => error: argument type mismatch at parameter `asc_or_desc' of
|
|
284
|
+
# `ascdesc' on AscDesc: expected :asc | :desc, got :bad
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The `# @rbs name: T` doc-style annotation, the `#: () -> T`
|
|
288
|
+
inline method-type comment, `# @rbs return: T`, attribute `#:`
|
|
289
|
+
casts, `# @rbs @ivar: T`, `# @rbs override`, and `# @rbs!` raw
|
|
290
|
+
RBS embedding all work — anything upstream rbs-inline accepts
|
|
291
|
+
flows through to Rigor's RBS environment as if you had hand-
|
|
292
|
+
written the equivalent `.rbs` file.
|
|
293
|
+
|
|
294
|
+
This is **not** RBS::Extended. The `# @rbs` comments are
|
|
295
|
+
upstream rbs-inline's grammar; the plugin transcribes them to
|
|
296
|
+
ordinary RBS at env build. RBS::Extended `%a{rigor:v1:…}`
|
|
297
|
+
directives, by contrast, are Rigor-only annotations that live
|
|
298
|
+
in `.rbs` files (see the rest of this chapter for those).
|
|
299
|
+
|
|
300
|
+
To enable it, add the plugin gem to your bundle and list it:
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
# .rigor.yml
|
|
304
|
+
plugins:
|
|
305
|
+
- rigor-rbs-inline
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Per file, opt in with the upstream `# rbs_inline: enabled`
|
|
309
|
+
magic comment at the top — files without it are unaffected.
|
|
310
|
+
|
|
311
|
+
Notes:
|
|
312
|
+
|
|
313
|
+
- The core `rigortype` analyzer stays zero-runtime-dependency
|
|
314
|
+
(ADR-0). The `rbs-inline` upstream library is a dependency
|
|
315
|
+
of the plugin gem, not of the core, so projects that don't
|
|
316
|
+
opt in pay nothing.
|
|
317
|
+
- A bare top-level `def` produces no RBS output through
|
|
318
|
+
upstream rbs-inline. Wrap method definitions in a class or
|
|
319
|
+
module when you need the annotation to take effect.
|
|
320
|
+
- A failed rbs-inline parse surfaces as a
|
|
321
|
+
`source-rbs-synthesis-failed` `:info` diagnostic; the file
|
|
322
|
+
falls back to no inline-RBS contribution and analysis
|
|
323
|
+
continues.
|
|
324
|
+
|
|
325
|
+
Full plugin documentation, configuration options (including
|
|
326
|
+
the `require_magic_comment: false` host-context override the
|
|
327
|
+
browser playground uses), and the caching contract:
|
|
328
|
+
[`plugins/rigor-rbs-inline/README.md`](../../plugins/rigor-rbs-inline/README.md).
|
|
329
|
+
|
|
330
|
+
## Falling back to `untyped`
|
|
331
|
+
|
|
332
|
+
When a method's signature involves a type RBS cannot express,
|
|
333
|
+
the conservative thing to do is `untyped`:
|
|
334
|
+
|
|
335
|
+
```rbs
|
|
336
|
+
def deserialize: (String) -> untyped
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`untyped` is a contract-free hatch — every method exists on
|
|
340
|
+
it, every argument shape is acceptable. Rigor's diagnostics
|
|
341
|
+
stay silent on `untyped` receivers. Use it for legitimately
|
|
342
|
+
dynamic boundaries (deserialisation, `eval`, plugin entry
|
|
343
|
+
points). The static analysis you lose is made up by the
|
|
344
|
+
honesty of admitting "this could be anything."
|
|
345
|
+
|
|
346
|
+
## Coming from PHPStan? The `@phpstan-assert` family
|
|
347
|
+
|
|
348
|
+
If you are familiar with PHPStan's PHPDoc annotations,
|
|
349
|
+
Rigor's `RBS::Extended` directives map directly onto the
|
|
350
|
+
post-return / conditional narrowing primitives PHPStan calls
|
|
351
|
+
"asserts" and "type-specifying functions." The behaviour is
|
|
352
|
+
identical:
|
|
353
|
+
|
|
354
|
+
> "After this method returns, the named argument is `T`."
|
|
355
|
+
|
|
356
|
+
That is `@phpstan-assert` in PHPStan and
|
|
357
|
+
`%a{rigor:v1:assert}` in Rigor.
|
|
358
|
+
|
|
359
|
+
| PHPStan PHPDoc | Rigor RBS::Extended | Effect |
|
|
360
|
+
| --- | --- | --- |
|
|
361
|
+
| `@phpstan-assert T $x` | `%a{rigor:v1:assert x is T}` | After this method returns normally, the caller's `x` is `T`. |
|
|
362
|
+
| `@phpstan-assert-if-true T $x` | `%a{rigor:v1:predicate-if-true x is T}` | If this method returns truthy, the caller's `x` is `T`. |
|
|
363
|
+
| `@phpstan-assert-if-false T $x` | `%a{rigor:v1:predicate-if-false x is T}` | If this method returns falsey, the caller's `x` is `T`. |
|
|
364
|
+
| `@phpstan-assert !T $x` | `%a{rigor:v1:assert x is ~T}` | After this method returns, the caller's `x` is **not** `T` (negation form). |
|
|
365
|
+
| `@phpstan-assert-if-true !T $x` | `%a{rigor:v1:predicate-if-true x is ~T}` | Conditional negation. Symmetric with `predicate-if-false`. |
|
|
366
|
+
|
|
367
|
+
Worked example — the canonical "assertNotNull" pattern from
|
|
368
|
+
PHPStan's docs:
|
|
369
|
+
|
|
370
|
+
```rbs
|
|
371
|
+
# sig/asserts.rbs
|
|
372
|
+
class Asserts
|
|
373
|
+
%a{rigor:v1:assert x is ~nil}
|
|
374
|
+
def self.not_nil: (untyped x) -> void
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
# lib/configure.rb
|
|
380
|
+
def configure(maybe)
|
|
381
|
+
Asserts.not_nil(maybe)
|
|
382
|
+
# maybe: (~nil), so .upcase resolves on the narrowed type
|
|
383
|
+
maybe.upcase
|
|
384
|
+
end
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Self-targeted forms are supported too — the PHPStan
|
|
388
|
+
analogue would be a method on `$this` that narrows
|
|
389
|
+
`$this`. Name the receiver with `self`:
|
|
390
|
+
|
|
391
|
+
```rbs
|
|
392
|
+
class Connection
|
|
393
|
+
%a{rigor:v1:assert self is Connected}
|
|
394
|
+
def assert_connected!: () -> void
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Rigor's directive grammar covers what PHPStan ships in the
|
|
399
|
+
`@phpstan-assert*` family. The directives **only fire from
|
|
400
|
+
RBS** (per ADR-5: strict on returns, lenient on parameters);
|
|
401
|
+
in PHPStan-land you can also write `@phpstan-assert` in
|
|
402
|
+
PHPDoc directly above the function — Rigor's equivalent is
|
|
403
|
+
the same RBS file's `def` line.
|
|
404
|
+
|
|
405
|
+
If you need plugin-side equivalents (when the assertion is
|
|
406
|
+
recognised by **call shape** rather than by sig — PHPStan's
|
|
407
|
+
"Type-Specifying Extensions"), see
|
|
408
|
+
[Chapter 9](09-plugins.md). The plugin contract surfaces
|
|
409
|
+
the same `Fact(target_kind: :self)` and
|
|
410
|
+
`Fact(target_kind: :parameter)` carriers that the directives
|
|
411
|
+
use, so a plugin author writes the equivalent of a PHPStan
|
|
412
|
+
`StaticMethodTypeSpecifyingExtension` from Ruby.
|
|
413
|
+
|
|
414
|
+
## When RBS cannot help — the plugin escape hatch
|
|
415
|
+
|
|
416
|
+
When a method's behaviour depends on the **shape of its
|
|
417
|
+
arguments at runtime** (`Lisp.eval([:+, 1, 2])` returns
|
|
418
|
+
Integer, but `Lisp.eval([:<, 1, 2])` returns bool), no RBS sig
|
|
419
|
+
can express the relationship. That is what plugins are for —
|
|
420
|
+
see [Chapter 9](09-plugins.md) and the
|
|
421
|
+
[examples/](../../examples/README.md) directory.
|
|
422
|
+
|
|
423
|
+
## What's next
|
|
424
|
+
|
|
425
|
+
Chapter 8 covers the rule catalogue — what each diagnostic
|
|
426
|
+
means, when it fires, and how to suppress it when it is wrong
|
|
427
|
+
or noisy.
|