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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -10
  3. data/data/builtins/ruby_core/array.yml +416 -392
  4. data/data/builtins/ruby_core/file.yml +42 -42
  5. data/data/builtins/ruby_core/hash.yml +302 -302
  6. data/data/builtins/ruby_core/io.yml +191 -191
  7. data/data/builtins/ruby_core/numeric.yml +321 -366
  8. data/data/builtins/ruby_core/proc.yml +124 -124
  9. data/data/builtins/ruby_core/range.yml +21 -21
  10. data/data/builtins/ruby_core/rational.yml +39 -39
  11. data/data/builtins/ruby_core/re.yml +65 -65
  12. data/data/builtins/ruby_core/set.yml +106 -106
  13. data/data/builtins/ruby_core/struct.yml +14 -14
  14. data/data/core_overlay/pathname.rbs +5 -0
  15. data/data/core_overlay/resolv.rbs +31 -0
  16. data/data/core_overlay/string_scanner.rbs +11 -5
  17. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  18. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  19. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  21. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  22. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  23. data/docs/handbook/01-getting-started.md +22 -34
  24. data/docs/handbook/06-classes.md +1 -1
  25. data/docs/handbook/07-rbs-and-extended.md +76 -101
  26. data/docs/handbook/08-understanding-errors.md +114 -247
  27. data/docs/handbook/09-plugins.md +54 -144
  28. data/docs/handbook/README.md +5 -3
  29. data/docs/handbook/appendix-liskov.md +4 -2
  30. data/docs/handbook/appendix-phpstan.md +14 -7
  31. data/docs/handbook/appendix-steep.md +4 -2
  32. data/docs/handbook/appendix-type-theory.md +3 -1
  33. data/docs/manual/02-cli-reference.md +103 -11
  34. data/docs/manual/03-configuration.md +16 -1
  35. data/docs/manual/04-diagnostics.md +36 -4
  36. data/docs/manual/06-baseline.md +35 -1
  37. data/docs/manual/08-skills.md +6 -1
  38. data/docs/manual/09-editor-integration.md +3 -2
  39. data/docs/manual/12-caching.md +17 -5
  40. data/docs/manual/15-type-protection-coverage.md +122 -1
  41. data/docs/manual/plugins/rigor-actioncable.md +32 -0
  42. data/docs/manual/plugins/rigor-devise.md +4 -2
  43. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
  46. data/lib/rigor/analysis/check_rules.rb +79 -19
  47. data/lib/rigor/analysis/incremental_session.rb +156 -11
  48. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  49. data/lib/rigor/analysis/run_cache_key.rb +27 -1
  50. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  52. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  53. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  54. data/lib/rigor/analysis/runner.rb +80 -8
  55. data/lib/rigor/bleeding_edge.rb +132 -7
  56. data/lib/rigor/cache/descriptor.rb +6 -1
  57. data/lib/rigor/cache/engine_source.rb +162 -0
  58. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  59. data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
  60. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
  61. data/lib/rigor/cache/store.rb +99 -24
  62. data/lib/rigor/cli/check_command.rb +61 -34
  63. data/lib/rigor/cli/check_invocation.rb +84 -0
  64. data/lib/rigor/cli/coverage_command.rb +15 -2
  65. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  66. data/lib/rigor/cli/doctor_command.rb +6 -8
  67. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  68. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  69. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  70. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  71. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  72. data/lib/rigor/cli/protection_renderer.rb +13 -0
  73. data/lib/rigor/cli/protection_report.rb +11 -3
  74. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  75. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  76. data/lib/rigor/cli/skill_command.rb +21 -1
  77. data/lib/rigor/cli/skill_deep_probe.rb +172 -0
  78. data/lib/rigor/cli/skill_describe.rb +75 -9
  79. data/lib/rigor/configuration.rb +45 -6
  80. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  81. data/lib/rigor/environment/default_libraries.rb +5 -4
  82. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  83. data/lib/rigor/environment/rbs_loader.rb +434 -75
  84. data/lib/rigor/environment.rb +38 -14
  85. data/lib/rigor/inference/expression_typer.rb +15 -0
  86. data/lib/rigor/inference/fork_map.rb +9 -0
  87. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  88. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  89. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
  92. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  93. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  95. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  96. data/lib/rigor/inference/mutation_widening.rb +30 -25
  97. data/lib/rigor/inference/narrowing.rb +3 -1
  98. data/lib/rigor/inference/optimistic_origin.rb +58 -0
  99. data/lib/rigor/inference/protection_scanner.rb +18 -2
  100. data/lib/rigor/inference/receiver_alias.rb +57 -0
  101. data/lib/rigor/inference/scope_indexer.rb +78 -27
  102. data/lib/rigor/inference/statement_evaluator.rb +40 -2
  103. data/lib/rigor/language_server/buffer_resolution.rb +6 -3
  104. data/lib/rigor/language_server/buffer_table.rb +68 -6
  105. data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
  106. data/lib/rigor/language_server/incremental_sync.rb +159 -0
  107. data/lib/rigor/language_server/project_context.rb +45 -0
  108. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  109. data/lib/rigor/language_server/server.rb +38 -9
  110. data/lib/rigor/language_server.rb +2 -0
  111. data/lib/rigor/plugin/base.rb +29 -2
  112. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  113. data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
  114. data/lib/rigor/protection/dependency_closure.rb +59 -0
  115. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  116. data/lib/rigor/protection/discovery_seed.rb +116 -0
  117. data/lib/rigor/protection/kill_signature.rb +31 -0
  118. data/lib/rigor/protection/mutation_cache.rb +355 -0
  119. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  120. data/lib/rigor/protection/mutator.rb +26 -4
  121. data/lib/rigor/runtime/jit.rb +63 -1
  122. data/lib/rigor/scope/discovery_index.rb +9 -0
  123. data/lib/rigor/scope.rb +52 -8
  124. data/lib/rigor/sig_gen/generator.rb +130 -31
  125. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  126. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  127. data/lib/rigor/sig_gen/renderer.rb +20 -1
  128. data/lib/rigor/sig_gen/write_result.rb +6 -4
  129. data/lib/rigor/sig_gen/writer.rb +195 -47
  130. data/lib/rigor/sig_gen.rb +1 -0
  131. data/lib/rigor/version.rb +1 -1
  132. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
  133. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  134. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  135. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  136. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  138. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  139. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  140. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  141. data/sig/rigor/cache.rbs +6 -0
  142. data/sig/rigor/inference/void_origin.rbs +21 -0
  143. data/sig/rigor/plugin/base.rbs +4 -3
  144. data/sig/rigor/scope.rbs +8 -0
  145. metadata +23 -3
@@ -100,27 +100,24 @@ Rigor sees a tightening.
100
100
 
101
101
  ## The directive grammar
102
102
 
103
- `RBS::Extended` lives at
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 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."
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, as a checked design
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 (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.
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 covers the rule catalogue — what each diagnostic
426
- means, when it fires, and how to suppress it when it is wrong
427
- or noisy.
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
- This chapter is the catalogue of diagnostics Rigor ships, the
4
- families they belong to, and how to suppress one when it is
5
- wrong (or move its severity around). It is the page to land on
6
- when a diagnostic surprises you, in either direction.
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 and reference fields
37
-
38
- Two extra fields ride along on every built-in diagnostic, for
39
- agents and dashboards consuming `rigor check --format json`
40
- (and on each rule in `rigor explain --format json`):
41
-
42
- - **`evidence_tier`** `high` / `medium` / `low`: Rigor's own
43
- confidence that the firing is a true positive, derived from
44
- the rule's gates, not its severity. `high` means a concrete,
45
- statically-known type with no metaprogramming escape (e.g.
46
- `call.undefined-method`); `medium` rests on a flow / inference
47
- proof with a documented false-positive envelope (e.g.
48
- `flow.always-truthy-condition`); `low` is a resolution- or
49
- coverage-gap signal that often means missing context rather
50
- than a bug (e.g. `call.unresolved-toplevel`). The tier never
51
- feeds severity that stays the `severity_profile:` decision.
52
- Informational helpers (`dump.type`) carry no tier.
53
- - **`documentation_url`** a stable link to the rule's entry in
54
- the published diagnostics catalogue.
55
-
56
- Both are presentation metadata. They never change whether a
57
- diagnostic fires.
58
-
59
- ## The rule catalogue
60
-
61
- Five families, each with one or more rules:
62
-
63
- ### `call.*` — call-site rules
64
-
65
- Fire when a method call's shape is wrong.
66
-
67
- | Rule | Fires when | Default severity |
68
- | --- | --- | --- |
69
- | `call.undefined-method` | The receiver class is statically known and the method is not defined on it (RBS or in-source). | error |
70
- | `call.wrong-arity` | The number of positional arguments does not satisfy any overload's arity. | error |
71
- | `call.argument-type-mismatch` | An argument's type provably does not satisfy the parameter contract (RBS or `RBS::Extended` `param:`). | error |
72
- | `call.possible-nil-receiver` | The receiver type is `T \| nil` and the method is not defined on `NilClass`. | error (warning under `lenient`) |
73
- | `call.unresolved-toplevel` | An implicit-self call at the top level (outside any `def` / `class` / `module`) resolves against no same-file `def`, `pre_eval:` monkey-patch, or `Kernel` / `Object` method — surfacing typos in standalone scripts. | warning under `balanced`, error under `strict`, suppressed under `lenient` |
74
-
75
- `call.*` rules are the highest-volume diagnostics on
76
- real-world code. They are also the most refined — every one
77
- fires only when Rigor can prove the underlying fact.
78
-
79
- ### `flow.*` flow-analysis rules
80
-
81
- Fire when the control flow itself is unsound.
82
-
83
- | Rule | Fires when | Default severity |
84
- | --- | --- | --- |
85
- | `flow.always-raises` | Every reachable evaluation of an expression raises (e.g. `n / 0` where `n: Integer`). | error |
86
- | `flow.unreachable-branch` | An `if` / `unless` / ternary's predicate is a syntactic literal AND the corresponding dead branch is non-empty. | warning |
87
- | `flow.always-truthy-condition` | The predicate of an `if` / `unless` / ternary is provably truthy (or falsey) by inferred type, with surgical skips inside loop bodies and on defensive predicate calls. | warning |
88
- | `flow.unreachable-clause` | A `case <local>; when <Class>` (or bare-class `case`/`in`) clause whose subject narrowing proves it can never match — disjoint from the subject's type, or already exhausted by an earlier clause. | info under `balanced`, warning under `strict`, info under `lenient` |
89
- | `flow.dead-assignment` | A plain local-variable write whose target name is never read in the same `def` body. | warning |
90
- | `flow.duplicate-hash-key` | Two entries of one Hash literal carry the same literal key (`{ a: 1, a: 2 }`, `m("x" => 1, "x" => 2)`) — the last entry wins silently at runtime. Literal keys only; `:a` vs `"a"` and `1` vs `1.0` are distinct keys and never compared. | warning |
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
+ [manualEvidence 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
- ### `assert.*` — runtime assertion rules
124
-
125
- | Rule | Fires when | Default severity |
126
- | --- | --- | --- |
127
- | `assert.type-mismatch` | An `assert_type("expected", value)` call's actual inferred type does not match the expected string. | error |
128
-
129
- ### `dump.*` debug helpers
130
-
131
- | Rule | Fires when | Default severity |
132
- | --- | --- | --- |
133
- | `dump.type` | `dump_type(value)` was called — emits an info diagnostic naming the inferred type. | info |
134
-
135
- `dump_type` is your introspection probe during debugging:
136
- sprinkle it through suspicious code, run `rigor check`, read
137
- the inferred types from the diagnostic stream.
138
-
139
- ## Severity profiles
140
-
141
- Rigor ships three named severity profiles that re-stamp the
142
- shipped severities:
143
-
144
- | Profile | Behaviour |
145
- | --- | --- |
146
- | `lenient` | Only proven rules stay `error` (`call.undefined-method`, `wrong-arity`, `assert.type-mismatch`); uncertain rules drop to `warning`, and several to `off`. For incremental adoption on legacy code. |
147
- | `balanced` (default) | Most rules → `error`; uncertain rules `warning`; `dump.type` → `info`. The shipped behaviour. |
148
- | `strict` | Nearly every rule → `error`. The exceptions: `call.self-undefined-method` stays `off` (opt-in only), and `flow.unreachable-clause` is `warning` (pending its false-positive gate). Suitable for new projects with no legacy noise. |
149
-
150
- Set in `.rigor.yml`:
151
-
152
- ```yaml
153
- severity_profile: strict
154
- ```
155
-
156
- ## Per-rule overrides
157
-
158
- Override a single rule's severity:
159
-
160
- ```yaml
161
- severity_overrides:
162
- call.argument-type-mismatch: warning
163
- def.return-type-mismatch: off
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