rigortype 0.3.0 → 0.3.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 +10 -10
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +11 -5
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +103 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +79 -19
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +27 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +80 -8
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +61 -34
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/doctor_command.rb +6 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +38 -14
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +68 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +38 -9
- data/lib/rigor/language_server.rb +2 -0
- data/lib/rigor/plugin/base.rb +29 -2
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +195 -47
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +21 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +23 -3
|
@@ -100,27 +100,24 @@ Rigor sees a tightening.
|
|
|
100
100
|
|
|
101
101
|
## The directive grammar
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
There are seven per-method directives, and they divide by
|
|
104
|
+
*when* the fact they carry becomes true: `return:` and `param:`
|
|
105
|
+
retype the signature itself, `predicate-if-true` /
|
|
106
|
+
`predicate-if-false` narrow a variable across the branches of a
|
|
107
|
+
condition, and `assert` / `assert-if-true` / `assert-if-false`
|
|
108
|
+
narrow one after the call returns. Each is one
|
|
109
|
+
`%a{rigor:v1:…}` annotation above the `def` it refines; they
|
|
110
|
+
stack, and order does not matter.
|
|
111
|
+
|
|
112
|
+
The exhaustive table — every directive, its payload syntax, and
|
|
113
|
+
what the `<type>` slot accepts (RBS class names, refinement
|
|
114
|
+
payloads, the parameterised and bounded forms, and where `~T`
|
|
115
|
+
negation is and is not allowed) — is
|
|
116
|
+
[manual — RBS::Extended annotations](../manual/16-rbs-extended-annotations.md#per-method-directives);
|
|
117
|
+
the normative rules for conflicts, merging and provenance are
|
|
104
118
|
[`docs/type-specification/rbs-extended.md`](../type-specification/rbs-extended.md).
|
|
105
|
-
The
|
|
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."
|
|
119
|
+
The rest of this chapter works through the directives one
|
|
120
|
+
example at a time.
|
|
124
121
|
|
|
125
122
|
## Refinement names
|
|
126
123
|
|
|
@@ -140,9 +137,8 @@ A short reference:
|
|
|
140
137
|
## Declaring conformance — `conforms-to`
|
|
141
138
|
|
|
142
139
|
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
|
|
145
|
-
assertion — whether or not any call site exercises it:
|
|
140
|
+
`class` / `module` declaration and asserts that the whole class
|
|
141
|
+
satisfies a named structural interface:
|
|
146
142
|
|
|
147
143
|
```rbs
|
|
148
144
|
%a{rigor:v1:conforms-to _RewindableStream}
|
|
@@ -153,13 +149,21 @@ end
|
|
|
153
149
|
```
|
|
154
150
|
|
|
155
151
|
If `MyBuffer` is missing a method the `_RewindableStream`
|
|
156
|
-
interface requires
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
structural compatibility
|
|
162
|
-
|
|
152
|
+
interface requires, Rigor reports
|
|
153
|
+
`rbs_extended.unsatisfied-conformance`; a class that satisfies
|
|
154
|
+
the interface is silent.
|
|
155
|
+
|
|
156
|
+
The reason to reach for this is that Rigor already checks
|
|
157
|
+
structural compatibility *implicitly*, wherever a value flows
|
|
158
|
+
into a position that needs a structural interface — so a class
|
|
159
|
+
nobody currently passes anywhere is never checked at all.
|
|
160
|
+
`conforms-to` turns the contract into a design assertion that
|
|
161
|
+
holds whether or not a call site exercises it, which is what a
|
|
162
|
+
library wants when the structural shape is the point. It is
|
|
163
|
+
purely additive: nothing that type-checked before stops doing
|
|
164
|
+
so because you added it. The stacking and diagnostic semantics
|
|
165
|
+
are in
|
|
166
|
+
[manual — `conforms-to`](../manual/16-rbs-extended-annotations.md#conforms-to--a-checked-structural-contract).
|
|
163
167
|
|
|
164
168
|
## Worked example: an assertion gate
|
|
165
169
|
|
|
@@ -181,6 +185,45 @@ end
|
|
|
181
185
|
The runtime side is whatever `assert_non_empty` does (raise
|
|
182
186
|
on empty, log, ...) — Rigor only reads the directive.
|
|
183
187
|
|
|
188
|
+
## Worked example: asserting a negative
|
|
189
|
+
|
|
190
|
+
An assertion payload can be negated with `~T`, which is how you
|
|
191
|
+
model the "this is definitely not nil any more" helper every
|
|
192
|
+
codebase grows:
|
|
193
|
+
|
|
194
|
+
```rbs
|
|
195
|
+
# sig/asserts.rbs
|
|
196
|
+
class Asserts
|
|
197
|
+
%a{rigor:v1:assert x is ~nil}
|
|
198
|
+
def self.not_nil: (untyped x) -> void
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# lib/configure.rb
|
|
204
|
+
def configure(maybe)
|
|
205
|
+
Asserts.not_nil(maybe)
|
|
206
|
+
# maybe: (~nil), so .upcase resolves on the narrowed type
|
|
207
|
+
maybe.upcase
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The target can also be the receiver itself — name it with
|
|
212
|
+
`self`, and the fact lands on the object the method was called
|
|
213
|
+
on:
|
|
214
|
+
|
|
215
|
+
```rbs
|
|
216
|
+
class Connection
|
|
217
|
+
%a{rigor:v1:assert self is Connected}
|
|
218
|
+
def assert_connected!: () -> void
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If PHPDoc's `@phpstan-assert` family is your mental model for
|
|
223
|
+
all of this, the reading is nearly one-for-one; the mapping
|
|
224
|
+
table is in
|
|
225
|
+
[appendix: Coming from PHPStan](appendix-phpstan.md#the-phpstan-assert-family).
|
|
226
|
+
|
|
184
227
|
## Worked example: a type predicate
|
|
185
228
|
|
|
186
229
|
```rbs
|
|
@@ -343,74 +386,6 @@ dynamic boundaries (deserialisation, `eval`, plugin entry
|
|
|
343
386
|
points). The static analysis you lose is made up by the
|
|
344
387
|
honesty of admitting "this could be anything."
|
|
345
388
|
|
|
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
389
|
## When RBS cannot help — the plugin escape hatch
|
|
415
390
|
|
|
416
391
|
When a method's behaviour depends on the **shape of its
|
|
@@ -422,6 +397,6 @@ see [Chapter 9](09-plugins.md) and the
|
|
|
422
397
|
|
|
423
398
|
## What's next
|
|
424
399
|
|
|
425
|
-
Chapter 8
|
|
426
|
-
|
|
427
|
-
|
|
400
|
+
Chapter 8 is about reading a diagnostic — what each rule
|
|
401
|
+
family claims, why one fires when you did not expect it, and
|
|
402
|
+
which layer to reach for when you want it quieter.
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Understanding errors
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
when
|
|
3
|
+
A diagnostic is Rigor telling you something it proved about
|
|
4
|
+
your code. This chapter is about *reading* one: what its parts
|
|
5
|
+
mean, what each rule family is really claiming, why one fires
|
|
6
|
+
when you did not expect it, why one stays silent when you did,
|
|
7
|
+
and how to work a freshly-adopted project down to a clean run.
|
|
8
|
+
|
|
9
|
+
The reference lives one door over. The full rule catalogue,
|
|
10
|
+
each rule's evidence tier, the severity-profile table, and the
|
|
11
|
+
exact syntax of every suppression form are in
|
|
12
|
+
[Diagnostics](../manual/04-diagnostics.md) in the manual; this
|
|
13
|
+
chapter links there rather than restating it.
|
|
7
14
|
|
|
8
15
|
## Anatomy of a diagnostic
|
|
9
16
|
|
|
@@ -33,251 +40,106 @@ fires, when it doesn't, the suppression token, the authored
|
|
|
33
40
|
severity, and the per-profile severity. `rigor explain` with
|
|
34
41
|
no argument prints the index of every shipped rule.
|
|
35
42
|
|
|
36
|
-
### Confidence
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
`flow.unreachable-branch`, `flow.always-truthy-condition`, and
|
|
93
|
-
`flow.unreachable-clause` are the **reachability family** — each
|
|
94
|
-
proves a branch or `case` clause is dead. `unreachable-clause`
|
|
95
|
-
is the newest member: it watches `case <local>; when <Class>`
|
|
96
|
-
(and bare-class `case`/`in`) and fires when an earlier clause
|
|
97
|
-
already covered a member's type or the clause is disjoint from
|
|
98
|
-
the subject. It ships at `:info` under `balanced` (one notch
|
|
99
|
-
below its siblings) while its corpus false-positive gate
|
|
100
|
-
finishes; bump it with `severity_overrides:` if you want it
|
|
101
|
-
louder.
|
|
102
|
-
|
|
103
|
-
### `def.*` — method-definition rules
|
|
104
|
-
|
|
105
|
-
Fire when the body of a method violates its declared
|
|
106
|
-
contract.
|
|
107
|
-
|
|
108
|
-
| Rule | Fires when | Default severity |
|
|
109
|
-
| --- | --- | --- |
|
|
110
|
-
| `def.return-type-mismatch` | The body's last expression's inferred type cannot satisfy the RBS-declared return type. Honors `%a{rigor:v1:return: <refinement>}` overrides. | warning under `balanced` profile, error under `strict` |
|
|
111
|
-
| `def.ivar-write-mismatch` | A later `@var = ...` write's concrete class disagrees with the first write's class in the same class body (NilClass-to-clear is allowlisted). | warning under `balanced` profile, error under `strict` |
|
|
112
|
-
| `def.method-visibility-mismatch` | An explicit-receiver call targets a `Nominal[X]` whose discovered method is `:private` in the surrounding class body. | error |
|
|
113
|
-
| `def.override-visibility-reduced` | An override reduces the visibility it inherits from a project-defined ancestor (public → protected/private, protected → private), breaking a caller that holds the supertype. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
114
|
-
| `def.override-return-widened` | An override's declared return widens the inherited return (covariance). Fires only on a proven violation when both sides carry an authored RBS signature. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
115
|
-
| `def.override-param-narrowed` | An override narrows an inherited parameter type (contravariance), comparing matching positional parameters. Requires an authored single-overload RBS signature on both sides. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
|
|
116
|
-
|
|
117
|
-
The three `def.override-*` rules are the Liskov Substitution
|
|
118
|
-
Principle signature rule applied across a project-defined
|
|
119
|
-
class/module hierarchy (superclass chain + included/prepended
|
|
120
|
-
modules, resolved cross-file). They are the conceptual subject of
|
|
43
|
+
### Confidence — reading the evidence tier
|
|
44
|
+
|
|
45
|
+
Every built-in diagnostic carries an `evidence_tier` —
|
|
46
|
+
`high` / `medium` / `low` — which is Rigor's own confidence
|
|
47
|
+
that the firing is a *true positive*, derived from the rule's
|
|
48
|
+
gates rather than from its severity. It is worth internalising
|
|
49
|
+
as a reading habit rather than a config knob: a `high` firing
|
|
50
|
+
(`call.undefined-method` on a concrete receiver) is almost
|
|
51
|
+
always a real bug and can be acted on directly, while a `low`
|
|
52
|
+
one (`call.unresolved-toplevel`) usually means the analyzer is
|
|
53
|
+
missing context — an unanalyzed file, a monkey-patch it never
|
|
54
|
+
saw — and reads better as "look at this" than "fix this". The
|
|
55
|
+
tier never feeds severity, and never changes whether a
|
|
56
|
+
diagnostic fires; it only routes your attention.
|
|
57
|
+
|
|
58
|
+
The per-rule tiers, and the `documentation_url` field that
|
|
59
|
+
rides alongside them in `--format json`, are in
|
|
60
|
+
[manual — Evidence tier](../manual/04-diagnostics.md#evidence-tier).
|
|
61
|
+
|
|
62
|
+
## The five families
|
|
63
|
+
|
|
64
|
+
Every rule ID reads `family.rule`, and the family tells you
|
|
65
|
+
what kind of proof failed. The catalogue — every rule, what it
|
|
66
|
+
fires on, its evidence tier — is in
|
|
67
|
+
[manual — Diagnostics](../manual/04-diagnostics.md#catalogue).
|
|
68
|
+
What follows is what each family is *about*.
|
|
69
|
+
|
|
70
|
+
**`call.*` — the call site's shape is wrong.** An undefined
|
|
71
|
+
method, an arity no signature accepts, an argument whose type
|
|
72
|
+
provably violates the parameter contract, a receiver that
|
|
73
|
+
might be `nil`. These are the highest-volume diagnostics on
|
|
74
|
+
real-world code, and also the most refined: every one of them
|
|
75
|
+
fires only when Rigor can prove the underlying fact about a
|
|
76
|
+
statically-known receiver, which is why a `call.*` firing is
|
|
77
|
+
usually worth reading first.
|
|
78
|
+
|
|
79
|
+
**`flow.*` — the control flow itself is unsound.** Something
|
|
80
|
+
provably raises on every path, a branch is dead, a `case`
|
|
81
|
+
clause can never match, a local is written and never read, a
|
|
82
|
+
Hash literal repeats a key. `flow.unreachable-branch`,
|
|
83
|
+
`flow.always-truthy-condition` and `flow.unreachable-clause`
|
|
84
|
+
form the **reachability family** — each proves that a piece of
|
|
85
|
+
code cannot run. `unreachable-clause` is the newest member and
|
|
86
|
+
deliberately quieter than its siblings (`:info` under
|
|
87
|
+
`balanced`) while its corpus false-positive gate finishes;
|
|
88
|
+
bump it with `severity_overrides:` if you want it louder.
|
|
89
|
+
|
|
90
|
+
**`def.*` — a definition violates the contract it declares.**
|
|
91
|
+
A body whose return drifts from the declared RBS return, an
|
|
92
|
+
instance variable written with two disagreeing types, an
|
|
93
|
+
explicit-receiver call into a private method. The three
|
|
94
|
+
`def.override-*` rules are the Liskov Substitution Principle's
|
|
95
|
+
signature rule applied across a project-defined hierarchy
|
|
96
|
+
(superclass chain plus included and prepended modules, resolved
|
|
97
|
+
cross-file): returns may narrow, parameters may widen,
|
|
98
|
+
visibility may not shrink. They are the conceptual subject of
|
|
121
99
|
[appendix: Liskov substitution](appendix-liskov.md).
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`off` drops the diagnostic from the result entirely — useful
|
|
167
|
-
when you want a profile-wide setting for most rules but
|
|
168
|
-
silence one specifically.
|
|
169
|
-
|
|
170
|
-
Family wildcards work in overrides too:
|
|
171
|
-
|
|
172
|
-
```yaml
|
|
173
|
-
severity_overrides:
|
|
174
|
-
call: warning # demote every call.* rule
|
|
175
|
-
dump: off # drop every dump.* rule
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Per-rule entries beat family-wildcard entries:
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
severity_overrides:
|
|
182
|
-
call: warning # every call.* → warning
|
|
183
|
-
call.undefined-method: error # except undefined-method, still error
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
YAML reserves the bareword `off`. If the stripped severity
|
|
187
|
-
seems not to apply, quote it: `"off"`. Same for `on`.
|
|
188
|
-
|
|
189
|
-
## In-source suppression
|
|
190
|
-
|
|
191
|
-
```ruby
|
|
192
|
-
"hello".no_such_method # rigor:disable call.undefined-method
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
The comment must be on the same line as the diagnostic. Use
|
|
196
|
-
the qualified rule, the family wildcard, or `all`:
|
|
197
|
-
|
|
198
|
-
```ruby
|
|
199
|
-
"hello".no_such_method # rigor:disable call
|
|
200
|
-
"hello".no_such_method # rigor:disable all
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
For multiline blocks, suppress at every line — Rigor does
|
|
204
|
-
not yet ship a `disable-block` syntax.
|
|
205
|
-
|
|
206
|
-
### File-scope suppression
|
|
207
|
-
|
|
208
|
-
When you need to silence a rule everywhere in a file —
|
|
209
|
-
typically a generated file, a fixture, or a vendored snippet
|
|
210
|
-
that triggers a known false positive — drop a single
|
|
211
|
-
`# rigor:disable-file` comment anywhere in the file:
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
# rigor:disable-file call.undefined-method
|
|
215
|
-
|
|
216
|
-
# This whole file is generated; the analyzer's call surface
|
|
217
|
-
# is mismatched with the runtime layer for these stubs.
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Convention is to put the comment near the top, but Rigor
|
|
221
|
-
scans every comment in the file so any placement works. The
|
|
222
|
-
same token forms apply: qualified rule, family wildcard, or
|
|
223
|
-
`all`. The line-scope `# rigor:disable` form continues to
|
|
224
|
-
work — the two compose, and any project-wide
|
|
225
|
-
`disable: [...]` in `.rigor.yml` also still applies.
|
|
226
|
-
|
|
227
|
-
## Project-wide suppression
|
|
228
|
-
|
|
229
|
-
```yaml
|
|
230
|
-
# .rigor.yml
|
|
231
|
-
disable:
|
|
232
|
-
- call.possible-nil-receiver
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Drops the rule project-wide. Heavier hammer than
|
|
236
|
-
`severity_overrides: { call.possible-nil-receiver: off }` —
|
|
237
|
-
both work; the choice is stylistic.
|
|
238
|
-
|
|
239
|
-
## Baseline diffing for CI
|
|
240
|
-
|
|
241
|
-
When you adopt Rigor on an existing codebase, you usually
|
|
242
|
-
inherit a long tail of legitimate-but-pre-existing diagnostics
|
|
243
|
-
that nobody is going to fix today. The pragmatic move is to
|
|
244
|
-
**snapshot the current state as a baseline** and then have CI
|
|
245
|
-
fail only on *new* diagnostics introduced by a PR:
|
|
246
|
-
|
|
247
|
-
```sh
|
|
248
|
-
# Once: capture the current diagnostic surface.
|
|
249
|
-
rigor check --format=json > rigor.baseline.json
|
|
250
|
-
git add rigor.baseline.json
|
|
251
|
-
git commit
|
|
252
|
-
|
|
253
|
-
# Per PR: compare against the committed baseline.
|
|
254
|
-
rigor diff rigor.baseline.json
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
`rigor diff` prints `+ NEW` rows for each diagnostic that
|
|
258
|
-
wasn't in the baseline and `- FIXED` rows for each that has
|
|
259
|
-
been resolved since. The exit code is `1` when any new
|
|
260
|
-
diagnostic appears and `0` otherwise — so adding a new
|
|
261
|
-
violation fails CI, but the legacy diagnostics recorded in
|
|
262
|
-
the baseline don't.
|
|
263
|
-
|
|
264
|
-
When you fix a row in the baseline, regenerate it with the
|
|
265
|
-
same `rigor check --format=json > rigor.baseline.json` so
|
|
266
|
-
the project tightens monotonically over time. The
|
|
267
|
-
`--format=json` form of `rigor diff` itself is also
|
|
268
|
-
available for editor / dashboard integrations.
|
|
269
|
-
|
|
270
|
-
`rigor diff` is the lightweight, ad-hoc form — a JSON file you
|
|
271
|
-
diff by hand in a CI script. Most projects instead adopt the
|
|
272
|
-
**managed baseline**: `rigor baseline generate` writes a
|
|
273
|
-
`.rigor-baseline.yml`, you point at it with the `baseline:`
|
|
274
|
-
config key, and from then on `rigor check` itself exits clean
|
|
275
|
-
on recorded diagnostics and surfaces only new ones — no
|
|
276
|
-
separate diff step. That is the path the
|
|
277
|
-
[`rigor-project-init` skill](../manual/14-rails-quickstart.md)
|
|
278
|
-
sets up for you; see [Baselines](../manual/06-baseline.md) for
|
|
279
|
-
the full workflow ([ADR-22](../adr/22-baseline-and-project-onboarding.md)
|
|
280
|
-
for the design).
|
|
101
|
+
**`assert.*` and `dump.*` — the introspection helpers.**
|
|
102
|
+
`assert.type-mismatch` fires when an `assert_type("expected",
|
|
103
|
+
value)` call disagrees with the inferred type, so a snippet in
|
|
104
|
+
this handbook is a test of the engine as much as an
|
|
105
|
+
illustration. `dump.type` is not a problem report at all — it
|
|
106
|
+
is your probe during debugging: sprinkle `dump_type(value)`
|
|
107
|
+
through suspicious code, run `rigor check`, and read the
|
|
108
|
+
inferred types straight out of the diagnostic stream.
|
|
109
|
+
|
|
110
|
+
## Turning a diagnostic down
|
|
111
|
+
|
|
112
|
+
Rigor gives you five layers, and picking the right one is
|
|
113
|
+
mostly a question of *how much* you want to say:
|
|
114
|
+
|
|
115
|
+
1. **`severity_profile:`** — the project's overall stance.
|
|
116
|
+
`lenient` for a legacy codebase you are easing Rigor into,
|
|
117
|
+
`balanced` for everyday work, `strict` for a project with
|
|
118
|
+
no legacy noise.
|
|
119
|
+
2. **`severity_overrides:`** — one rule (or one family) at a
|
|
120
|
+
different severity from the rest of the profile. The right
|
|
121
|
+
layer when a rule is *useful but not blocking* for you.
|
|
122
|
+
3. **`disable:`** — the rule is off project-wide. Heavier than
|
|
123
|
+
an override to `off`; both work, and the choice is mostly
|
|
124
|
+
stylistic.
|
|
125
|
+
4. **`# rigor:disable` / `# rigor:disable-file`** — this line,
|
|
126
|
+
or this file. The right layer when the analyzer is wrong
|
|
127
|
+
*here* and right everywhere else. Prefer it to a
|
|
128
|
+
project-wide switch: it keeps the exception visible next to
|
|
129
|
+
the code that needed it, and it is the thing you later
|
|
130
|
+
promote into an `RBS::Extended` directive.
|
|
131
|
+
5. **A [baseline](../manual/06-baseline.md)** — the whole
|
|
132
|
+
existing backlog, recorded rather than hidden, so new
|
|
133
|
+
diagnostics still surface. This is the layer to reach for
|
|
134
|
+
on adoption day, and the one to reach for *instead of*
|
|
135
|
+
`disable:` when the rule is genuinely finding things you
|
|
136
|
+
have simply not fixed yet.
|
|
137
|
+
|
|
138
|
+
The exact syntax of all five — profile table, override
|
|
139
|
+
precedence, the three suppression forms, the baseline file and
|
|
140
|
+
its `rigor baseline` commands — is in
|
|
141
|
+
[manual — Diagnostics](../manual/04-diagnostics.md#severity-profiles)
|
|
142
|
+
and [manual — Baselines](../manual/06-baseline.md).
|
|
281
143
|
|
|
282
144
|
## Why a diagnostic might NOT fire when you expected one
|
|
283
145
|
|
|
@@ -357,6 +219,11 @@ The pragmatic loop on a project that just adopted Rigor:
|
|
|
357
219
|
`# rigor:disable` lines into `RBS::Extended` directives
|
|
358
220
|
so the analyzer learns the real contract.
|
|
359
221
|
|
|
222
|
+
On a codebase too large for step 2 in one sitting, record the
|
|
223
|
+
existing diagnostics as a
|
|
224
|
+
[baseline](../manual/06-baseline.md) first and run the loop
|
|
225
|
+
against what CI newly surfaces.
|
|
226
|
+
|
|
360
227
|
A clean `rigor check` run is the goal; a green CI badge says
|
|
361
228
|
"every diagnostic that fires is one we accept."
|
|
362
229
|
|