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
data/docs/manual/06-baseline.md
CHANGED
|
@@ -4,7 +4,11 @@ A **baseline** records the diagnostics a project already has,
|
|
|
4
4
|
so `rigor check` can stay silent about them and surface only
|
|
5
5
|
what is *new*. It is the pragmatic on-ramp for adopting Rigor
|
|
6
6
|
on an existing codebase: you do not have to reach zero
|
|
7
|
-
diagnostics before the check becomes useful in CI.
|
|
7
|
+
diagnostics before the check becomes useful in CI. It is also
|
|
8
|
+
what the [`rigor-project-init` skill](08-skills.md) snapshots
|
|
9
|
+
for you at onboarding
|
|
10
|
+
([ADR-22](../adr/22-baseline-and-project-onboarding.md) is the
|
|
11
|
+
design).
|
|
8
12
|
|
|
9
13
|
## The baseline file
|
|
10
14
|
|
|
@@ -89,6 +93,36 @@ unless you specifically need per-message discrimination, and
|
|
|
89
93
|
expect to `regenerate` a `message`-mode baseline after upgrading
|
|
90
94
|
Rigor.
|
|
91
95
|
|
|
96
|
+
## The ad-hoc form — `rigor diff`
|
|
97
|
+
|
|
98
|
+
A managed baseline is not the only way to fail CI on *new*
|
|
99
|
+
diagnostics only. The lightweight alternative keeps a plain JSON
|
|
100
|
+
snapshot in the repository and compares against it explicitly:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
# Once: capture the current diagnostic surface.
|
|
104
|
+
rigor check --format=json > rigor.baseline.json
|
|
105
|
+
git add rigor.baseline.json
|
|
106
|
+
|
|
107
|
+
# Per PR: compare against the committed snapshot.
|
|
108
|
+
rigor diff rigor.baseline.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
[`rigor diff`](02-cli-reference.md#rigor-diff) prints a `+ NEW`
|
|
112
|
+
row for every diagnostic absent from the snapshot and a
|
|
113
|
+
`- FIXED` row for every one resolved since, and exits `1` when
|
|
114
|
+
anything is new — so a PR that adds a violation fails while the
|
|
115
|
+
recorded legacy ones stay quiet. `--format=json` is available
|
|
116
|
+
for editor and dashboard integrations. Regenerate the snapshot
|
|
117
|
+
with the same `rigor check --format=json` redirection whenever
|
|
118
|
+
you fix a row, and the project tightens monotonically.
|
|
119
|
+
|
|
120
|
+
The difference from a managed baseline is where the knowledge
|
|
121
|
+
lives: `rigor diff` is a separate step your CI script has to
|
|
122
|
+
run, while a `baseline:` file makes `rigor check` itself exit
|
|
123
|
+
clean on recorded diagnostics. Prefer the managed form unless
|
|
124
|
+
you specifically want the raw JSON snapshot.
|
|
125
|
+
|
|
92
126
|
## Working a baseline down
|
|
93
127
|
|
|
94
128
|
`rigor triage` summarises a diagnostic stream — rule
|
data/docs/manual/08-skills.md
CHANGED
|
@@ -106,6 +106,7 @@ source checkout. The `rigor skill` command surfaces them:
|
|
|
106
106
|
|
|
107
107
|
```sh
|
|
108
108
|
rigor skill describe # probe the project + recommend the next skill (alias: rigor describe)
|
|
109
|
+
rigor skill describe --deep # same, but run `rigor check` first and route on its result
|
|
109
110
|
rigor skill --list # name + absolute path for each bundled skill
|
|
110
111
|
rigor skill <name> # print the SKILL.md body (with a references/ header)
|
|
111
112
|
rigor skill --full <name> # the body + every references/*.md inline (complete procedure)
|
|
@@ -113,7 +114,11 @@ rigor skill --path <name> # one-line absolute SKILL.md path, for a file-readin
|
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
`rigor skill describe` is the recommendation engine driven by
|
|
116
|
-
`rigor-next-steps
|
|
117
|
+
`rigor-next-steps`. It is a presence-only probe by default — cheap and
|
|
118
|
+
side-effect-free; add `--deep` when you want the recommendation to come
|
|
119
|
+
from a real analysis instead
|
|
120
|
+
([CLI reference](02-cli-reference.md#describe---deep)).
|
|
121
|
+
`rigor skill rigor-project-init` is the
|
|
117
122
|
canonical way to hand an agent the onboarding workflow without pointing
|
|
118
123
|
it at the repository. The `list` / `print` / `path` verb spellings are
|
|
119
124
|
deprecated (removed in v0.3.0). See [CLI reference](02-cli-reference.md#rigor-skill).
|
|
@@ -18,6 +18,7 @@ design + capability matrix lives in
|
|
|
18
18
|
|
|
19
19
|
| LSP method | Behaviour |
|
|
20
20
|
|---|---|
|
|
21
|
+
| `textDocument/didOpen` / `didChange` / `didClose` | Incremental sync (`TextDocumentSyncKind::Incremental`): a `didChange` carries range edits, which are spliced into the buffer the server already holds instead of re-sending and re-parsing the whole document on every keystroke. Positions are read as UTF-16 code-unit offsets (`positionEncoding: "utf-16"`), so an emoji or other non-BMP character counts as two. The full-text change form stays supported. |
|
|
21
22
|
| `textDocument/publishDiagnostics` | Pushed on every `didChange`, 200ms debounced. Severity / rule / source map directly to Rigor's diagnostic taxonomy. |
|
|
22
23
|
| `textDocument/hover` | Type-aware markdown. Per-node-class dispatch surfaces receiver type + RBS signature for method calls, FQN + singleton type + defined-in path for constants, narrowed type + bound-at for locals, canonical refinement names (`non-empty-string`, …) for `Refined` / `Difference` carriers. |
|
|
23
24
|
| `textDocument/completion` | Method completion after `.` (driven by inferred receiver type), constant-path completion after `::`. Composite receivers (Union → intersection of methods, Tuple / HashShape → ancestor nominal, Refined → underlying nominal) handled. Parse-recovery sentinel makes mid-edit `obj.` / `Foo::` buffers work. |
|
|
@@ -234,8 +235,8 @@ Cold start is dominated by RBS environment build; warm starts
|
|
|
234
235
|
|
|
235
236
|
LSP v1 + v2 landed in v0.1.6 and ship in the `0.1.x` line. Queued
|
|
236
237
|
follow-ups (`textDocument/signatureHelp`, hash-key completion,
|
|
237
|
-
`textDocument/definition`,
|
|
238
|
-
|
|
238
|
+
`textDocument/definition`, Ractor pool dispatch,
|
|
239
|
+
codeAction / rename / semanticTokens / inlayHint)
|
|
239
240
|
are demand-driven; the current queue is the open
|
|
240
241
|
[`area:editor` issues](https://github.com/rigortype/rigor/issues?q=is%3Aissue+is%3Aopen+label%3Aarea%3Aeditor).
|
|
241
242
|
|
data/docs/manual/12-caching.md
CHANGED
|
@@ -28,12 +28,23 @@ on:
|
|
|
28
28
|
- the **content** of the source and `.rbs` files that fed it,
|
|
29
29
|
- the **gems** in play, by name and locked version,
|
|
30
30
|
- the active **plugins**, by ID and version,
|
|
31
|
-
- the relevant **configuration
|
|
31
|
+
- the relevant **configuration**,
|
|
32
|
+
- **Rigor itself**, by version.
|
|
32
33
|
|
|
33
34
|
Change any of those and the dependent entries are recomputed
|
|
34
35
|
automatically. A corrupt or unreadable entry is treated as a
|
|
35
36
|
miss and overwritten — bad cache state cannot wedge a run.
|
|
36
37
|
|
|
38
|
+
Rigor's version identifies its own code only for a released gem.
|
|
39
|
+
If you run Rigor from a git checkout — a working copy you are
|
|
40
|
+
patching, or `gem "rigor", github:` tracking a branch, where two
|
|
41
|
+
commits share one version number — the entries that hold
|
|
42
|
+
analysis results are keyed on the content of Rigor's own source
|
|
43
|
+
as well, so editing the analyzer and re-running recomputes
|
|
44
|
+
instead of replaying the previous answer. That costs one pass
|
|
45
|
+
over Rigor's source per run (about 20 ms); an installed gem
|
|
46
|
+
neither pays it nor needs it.
|
|
47
|
+
|
|
37
48
|
The cache is also schema-versioned: after a Rigor upgrade that
|
|
38
49
|
changes the cache format, the stale cache is purged on the
|
|
39
50
|
first writable run.
|
|
@@ -157,10 +168,11 @@ affected files in parallel just as a full run does.
|
|
|
157
168
|
|
|
158
169
|
The snapshot lives under the cache directory (`.rigor/cache`)
|
|
159
170
|
and is keyed by a fingerprint of your configuration, your locked
|
|
160
|
-
gems, your project's own `sig/` RBS,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
gems, your project's own `sig/` RBS, the Rigor version, and — if
|
|
172
|
+
you run Rigor from a checkout — the content of Rigor's own
|
|
173
|
+
source. Change any of those and the snapshot is dropped and the
|
|
174
|
+
next run is a full one, so an incremental run can never serve a
|
|
175
|
+
stale result. (The fingerprint keys on the analysis *roots* — e.g.
|
|
164
176
|
`["lib"]` — not the expanded file list, so adding or removing a
|
|
165
177
|
file *under* those roots does **not** drop the snapshot: the
|
|
166
178
|
incremental session re-analyzes the added files and the
|
|
@@ -48,6 +48,17 @@ power. `--threshold=RATIO` turns it into a CI gate (exit `1`
|
|
|
48
48
|
below the ratio) and `--format=json` carries the structured
|
|
49
49
|
fields.
|
|
50
50
|
|
|
51
|
+
Some `protected` sites are typed only through call-site
|
|
52
|
+
[parameter inference](03-configuration.md) (`parameter_inference:`
|
|
53
|
+
— an undeclared parameter seeded from what its callers pass)
|
|
54
|
+
rather than a declaration — a lower bound that can only widen, so
|
|
55
|
+
none of Rigor's negative rules fire against it yet
|
|
56
|
+
([ADR-67](../adr/67-parameter-type-inference.md) WD6b). The text
|
|
57
|
+
report calls these out as "lower-bound-typed" when there are any,
|
|
58
|
+
and the JSON form always carries the count as `lower_bound_typed`.
|
|
59
|
+
It is a sub-count *within* `protected`, never subtracted from it —
|
|
60
|
+
the protected ratio itself is unaffected.
|
|
61
|
+
|
|
51
62
|
On a large project, `--workers=N` fork-parallelizes the scan
|
|
52
63
|
(both the parameter-inference pre-pass and the per-file scan),
|
|
53
64
|
with output byte-identical to a sequential run. The worker count
|
|
@@ -74,11 +85,117 @@ real "add a type here" site, surfaced with no guesswork.
|
|
|
74
85
|
|
|
75
86
|
It runs many analyses, so it defaults to the **git-changed** `.rb`
|
|
76
87
|
files (pass explicit paths to widen — whole-project is minutes) and
|
|
77
|
-
is an opt-in CI deep-dive, not an interactive check.
|
|
88
|
+
is an opt-in CI deep-dive, not an interactive check. `--workers=N`
|
|
89
|
+
applies here too, and it is the main lever on a wide run: the
|
|
90
|
+
whole-project pre-pass is paid once, then the per-file measurement —
|
|
91
|
+
which is nearly all of the wall time — is fork-mapped across
|
|
92
|
+
workers, byte-identically to a sequential run.
|
|
93
|
+
|
|
94
|
+
The framing is
|
|
78
95
|
always *effectiveness / where to add a type*, never "your code is
|
|
79
96
|
broken": a surviving breakage at a `Dynamic` site is a place
|
|
80
97
|
the type net does not reach.
|
|
81
98
|
|
|
99
|
+
### Re-running it is cheap
|
|
100
|
+
|
|
101
|
+
Each file's measurement is cached and served back while nothing
|
|
102
|
+
that could change it has moved — the file itself, any file it was
|
|
103
|
+
recorded as reading from, the resolved configuration, your `sig/`,
|
|
104
|
+
the gem set, the engine version, `--limit` / `--seed`, and which
|
|
105
|
+
bleeding-edge features you have adopted. Edit one file and that
|
|
106
|
+
file plus the files recorded as reading from it are re-measured;
|
|
107
|
+
the rest are served. A one-line stderr report says what happened,
|
|
108
|
+
so `--format=json` stdout stays clean:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
coverage: mutation cache — re-measured 2 file(s), 350 served from cache.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The cache reads the cross-file dependency edges that a
|
|
115
|
+
`rigor check --incremental` run records, so warm one once per
|
|
116
|
+
project:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
rigor check --incremental # records the edges
|
|
120
|
+
rigor coverage --protection --mutation lib # measures, then caches
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Without that snapshot — or under `--no-cache`, or with
|
|
124
|
+
`dependent-closure-kill-oracle` adopted, whose verdicts depend on
|
|
125
|
+
*other* files' diagnostics and so cannot be validated per file —
|
|
126
|
+
every file is re-measured and the stderr line names the reason. A
|
|
127
|
+
file the snapshot does not know about is re-measured rather than
|
|
128
|
+
served: the cache never guesses.
|
|
129
|
+
|
|
130
|
+
### Which sites Tier 2 measures
|
|
131
|
+
|
|
132
|
+
Tier 2 only mutates a site where Rigor holds a *concrete* receiver
|
|
133
|
+
type — at a `Dynamic` receiver the breakage would survive by
|
|
134
|
+
construction, which measures nothing. Today it decides that from
|
|
135
|
+
the file alone, so a call on a project class declared in a
|
|
136
|
+
**sibling** file (`Post.where`, `Account.find`) reads `Dynamic` and
|
|
137
|
+
the site is left out of the denominator entirely, even though Tier
|
|
138
|
+
1 counts it.
|
|
139
|
+
|
|
140
|
+
The [`discovery-seeded-mutation-sites`](02-cli-reference.md#rigor-show-bleedingedge)
|
|
141
|
+
bleeding-edge feature closes that gap, giving Tier 2 the same
|
|
142
|
+
cross-file view Tier 1 has — on *both* halves of the measurement,
|
|
143
|
+
site selection and the re-analysis that decides whether a mutation
|
|
144
|
+
was caught, so an admitted site is one a breakage can really trip:
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
# .rigor.yml
|
|
148
|
+
bleeding_edge:
|
|
149
|
+
- discovery-seeded-mutation-sites
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It is **off by default and opt-in on purpose**. Those sites are
|
|
153
|
+
mostly ones the type net does not yet catch, so admitting them
|
|
154
|
+
*lowers* the reported effectiveness ratio on unchanged code — and
|
|
155
|
+
`--threshold=RATIO` exits 1 below a ratio you pin in CI. Adopt it
|
|
156
|
+
when you are ready to re-baseline that number; it is the intended
|
|
157
|
+
default at a future major.
|
|
158
|
+
|
|
159
|
+
### Where a catch is allowed to land
|
|
160
|
+
|
|
161
|
+
Deciding whether a breakage was caught is a separate question from
|
|
162
|
+
which sites are measured, and it has the same blind spot in the
|
|
163
|
+
opposite direction. Tier 2 re-analyses the mutated file and looks
|
|
164
|
+
at *that file's* diagnostics — so the most valuable catch Rigor
|
|
165
|
+
delivers is scored as a miss. Change what a method returns and the
|
|
166
|
+
error appears in its **callers**, which is exactly the cross-file
|
|
167
|
+
reach the analyzer exists for.
|
|
168
|
+
|
|
169
|
+
The [`dependent-closure-kill-oracle`](02-cli-reference.md#rigor-show-bleedingedge)
|
|
170
|
+
bleeding-edge feature re-analyses the mutated file **and the files
|
|
171
|
+
that depend on it**, and counts the breakage as caught when a new
|
|
172
|
+
diagnostic appears anywhere in that set:
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
# .rigor.yml
|
|
176
|
+
bleeding_edge:
|
|
177
|
+
- dependent-closure-kill-oracle
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
It composes with the site seed above — one decides what is
|
|
181
|
+
measured, the other where a catch may land — and either can be
|
|
182
|
+
adopted alone. The mutated file's verdict is unchanged, so this can
|
|
183
|
+
only **add** kills: the ratio moves up or not at all, and it cannot
|
|
184
|
+
turn a `--threshold` build red. A number recorded under it is still
|
|
185
|
+
not comparable with one recorded without it.
|
|
186
|
+
|
|
187
|
+
It costs more: each surviving breakage is re-analysed once per
|
|
188
|
+
dependent file (the caught ones stop at the mutated file), and the
|
|
189
|
+
dependency graph itself is one extra whole-project pass per run —
|
|
190
|
+
about a third more wall time on Rigor's own `lib`.
|
|
191
|
+
|
|
192
|
+
Worth knowing before you pay that: on the two codebases measured so
|
|
193
|
+
far (Rigor's own `lib`, redmine's `app/models`) it added **no**
|
|
194
|
+
kills. A breakage that survives there is one the analyzer reports
|
|
195
|
+
*nowhere* — not one it reports in a caller — which is a statement
|
|
196
|
+
about where the type net has teeth, and exactly what the
|
|
197
|
+
unprotected-site list is telling you.
|
|
198
|
+
|
|
82
199
|
What *does* reach it is your tests.
|
|
83
200
|
|
|
84
201
|
## The fused view — types **and** tests (`--with-tests`)
|
|
@@ -236,6 +353,10 @@ them as a deep-dive, not a per-keystroke check:
|
|
|
236
353
|
(suite runtime)`. A fast, well-scoped test command is the biggest
|
|
237
354
|
lever.
|
|
238
355
|
- **Cap with `--limit`** on `--include-dynamic` or large files.
|
|
356
|
+
- **Spread with `--workers=N`** on `--mutation` (and Tier 1). The
|
|
357
|
+
fused `--with-tests` tier stays sequential — the suite hook shells
|
|
358
|
+
out, and concurrent runs would race — so widen it with scope and
|
|
359
|
+
`--limit` instead.
|
|
239
360
|
|
|
240
361
|
## In CI
|
|
241
362
|
|
|
@@ -44,6 +44,30 @@ or `stream_for record`) — the absence of a literal match doesn't prove
|
|
|
44
44
|
the stream is invalid. Non-`Channel` receivers and non-literal stream
|
|
45
45
|
arguments pass through silently.
|
|
46
46
|
|
|
47
|
+
## `#receive(data)` parameter typing
|
|
48
|
+
|
|
49
|
+
The plugin also carries an [ADR-28](../../adr/28-path-scoped-protocol-contracts.md)
|
|
50
|
+
path-scoped protocol contract: inside any `#receive(data)` defined
|
|
51
|
+
under `channel_search_paths`, `data` types as `Hash` instead of
|
|
52
|
+
`Dynamic[Top]`. `#receive` is ActionCable's framework-dispatched
|
|
53
|
+
catch-all action — invoked with the decoded JSON payload when an
|
|
54
|
+
incoming message carries no `"action"` key — so the parameter shape is
|
|
55
|
+
uniform across every channel.
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/channels/chat_channel.rb
|
|
59
|
+
class ChatChannel < ApplicationCable::Channel
|
|
60
|
+
def receive(data)
|
|
61
|
+
data["body"] # data: Hash
|
|
62
|
+
data.no_such_method # error: call.undefined-method
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Custom action methods (`def speak(data)`) are not covered — their
|
|
68
|
+
names are project-chosen, and a protocol contract names a single fixed
|
|
69
|
+
method. Only `#receive` is a reserved, uniformly-shaped hook.
|
|
70
|
+
|
|
47
71
|
## Configuration
|
|
48
72
|
|
|
49
73
|
```yaml
|
|
@@ -54,6 +78,11 @@ plugins:
|
|
|
54
78
|
channel_base_classes: ["ApplicationCable::Channel", "ActionCable::Channel::Base"] # default
|
|
55
79
|
```
|
|
56
80
|
|
|
81
|
+
`channel_search_paths` also retargets the `#receive` protocol
|
|
82
|
+
contract's glob — including for multiple configured roots — so a
|
|
83
|
+
custom channel directory gets the same `data: Hash` typing as the
|
|
84
|
+
default.
|
|
85
|
+
|
|
57
86
|
## Limitations
|
|
58
87
|
|
|
59
88
|
- **Direct-superclass match only.** An indirect chain (`AdminChannel <
|
|
@@ -68,6 +97,9 @@ plugins:
|
|
|
68
97
|
than directly in the channel body) is out of scope.
|
|
69
98
|
- **Bare `broadcast(...)`** without an explicit `ActionCable.server`
|
|
70
99
|
receiver is skipped to avoid false positives on unrelated methods.
|
|
100
|
+
- **The `#receive` contract is path-scoped, not class-scoped** (ADR-28):
|
|
101
|
+
any `def receive(data)` defined anywhere under `channel_search_paths`
|
|
102
|
+
is typed, even on a class that isn't an ActionCable channel.
|
|
71
103
|
|
|
72
104
|
## Plugin internals
|
|
73
105
|
|
|
@@ -64,7 +64,9 @@ The macro manifest (the trait registry mapping each strategy to its
|
|
|
64
64
|
module), the concern re-targeting walk, the demo, and the
|
|
65
65
|
contract surfaces this plugin exercises are in the
|
|
66
66
|
[plugin's README](../../../plugins/rigor-devise/README.md);
|
|
67
|
-
[
|
|
68
|
-
|
|
67
|
+
[`docs/internal-spec/macro-substrate.md`](../../internal-spec/macro-substrate.md)
|
|
68
|
+
specifies the Tier B trait registry generally, and
|
|
69
|
+
[handbook chapter 9](../../handbook/09-plugins.md) is the
|
|
70
|
+
orientation. To write a plugin, see
|
|
69
71
|
[`examples/`](../../../examples/README.md) and the
|
|
70
72
|
[`rigor-plugin-author`](../08-skills.md) skill.
|
|
@@ -45,6 +45,28 @@ plugin id/version + config), so an unchanged second run skips the parse.
|
|
|
45
45
|
| Rule | Severity | Fires when |
|
|
46
46
|
| --- | --- | --- |
|
|
47
47
|
| `plugin.rbs-inline.source-rbs-synthesis-failed` | info | rbs-inline could not parse a file; analysis falls back to no inline-RBS contribution and the diagnostic carries the upstream error |
|
|
48
|
+
| `plugin.rbs-inline.source-rbs-annotation-not-honoured` | info | an annotation parsed successfully but contributed nothing — the file's other annotations still apply. Today this means the `# @rbs module-self: Foo` spelling; see below |
|
|
49
|
+
|
|
50
|
+
## Which inline-RBS dialect Rigor reads
|
|
51
|
+
|
|
52
|
+
There are two implementations of inline RBS: the
|
|
53
|
+
[`rbs-inline` gem](https://github.com/soutaro/rbs-inline), which this plugin
|
|
54
|
+
runs, and the `RBS::InlineParser` built into `rbs` 4.x. **Rigor reads the
|
|
55
|
+
gem's dialect** ([ADR-32](../../adr/32-rbs-inline-comment-ingestion.md) WD11).
|
|
56
|
+
They overlap almost entirely — `#:`, `@rbs` method types, `def self.`,
|
|
57
|
+
instance-variable annotations, `@rbs skip` all behave identically — but they
|
|
58
|
+
are not the same grammar, and one difference bites in practice:
|
|
59
|
+
|
|
60
|
+
| you write | Rigor honours it |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `# @rbs module-self Comparable` | yes |
|
|
63
|
+
| `# @rbs module-self: Comparable` | **no** — this is the spelling in rbs's own `docs/inline.md` |
|
|
64
|
+
|
|
65
|
+
Rigor reports the second form as
|
|
66
|
+
`plugin.rbs-inline.source-rbs-annotation-not-honoured` rather than dropping it
|
|
67
|
+
in silence. Constructs the gem supports and the built-in parser does not —
|
|
68
|
+
`@rbs generic T`, `@rbs!` embedded RBS blocks, `@rbs inherits`, method
|
|
69
|
+
visibility — all work here.
|
|
48
70
|
|
|
49
71
|
## Configuration
|
|
50
72
|
|
|
@@ -36,6 +36,12 @@ module Rigor
|
|
|
36
36
|
Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
37
37
|
].freeze
|
|
38
38
|
|
|
39
|
+
# ADR-53 Track B — the node classes the shared {RuleWalk} dispatches to this collector: the consumer
|
|
40
|
+
# nodes whose slots are value positions. Prism node classes are leaves, so class-equality dispatch
|
|
41
|
+
# matches the legacy walk's `case`. No context gate — a value-context void use is a use wherever the
|
|
42
|
+
# DFS reaches it, loops and blocks included.
|
|
43
|
+
NODE_CLASSES = [Prism::CallNode, *WRITE_NODE_CLASSES].freeze
|
|
44
|
+
|
|
39
45
|
Result = Data.define(:void_node, :origin)
|
|
40
46
|
|
|
41
47
|
def initialize(scope_index)
|
|
@@ -43,14 +49,27 @@ module Rigor
|
|
|
43
49
|
@results = []
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
52
|
+
# Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
|
|
53
|
+
# {RuleWalk} against. Walks the whole subtree once, checking each consumer node's value slots.
|
|
54
|
+
# Returns one {Result} per value-context use of a recovered-`void` call.
|
|
48
55
|
# @return [Array<Result>]
|
|
49
56
|
def collect(root)
|
|
50
57
|
walk(root)
|
|
51
58
|
@results.freeze
|
|
52
59
|
end
|
|
53
60
|
|
|
61
|
+
# {RuleWalk} entry point: the legacy walk's per-node slot inspection, invoked at every consumer node
|
|
62
|
+
# under the shared traversal contract. The `context` is unused — every consumer node is inspected.
|
|
63
|
+
def visit(node, _context = nil)
|
|
64
|
+
inspect_consumer(node)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The accumulated result, frozen the same way `#collect` returns it — used by {RuleWalk}-driven
|
|
68
|
+
# callers after the walk completes.
|
|
69
|
+
def results
|
|
70
|
+
@results.freeze
|
|
71
|
+
end
|
|
72
|
+
|
|
54
73
|
private
|
|
55
74
|
|
|
56
75
|
def walk(node)
|
|
@@ -97,7 +97,6 @@ module Rigor
|
|
|
97
97
|
collectors = node_collectors || run_node_collectors(path, root, scope_index)
|
|
98
98
|
diagnostics = collectors[:main_pass].results.dup
|
|
99
99
|
diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
|
|
100
|
-
diagnostics.concat(void_value_use_diagnostics(path, root, scope_index))
|
|
101
100
|
COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
|
|
102
101
|
diagnostics.concat(send(builder, path, collectors[role].results))
|
|
103
102
|
end
|
|
@@ -109,10 +108,13 @@ module Rigor
|
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
# The per-collector diagnostic builders {.diagnose} folds over, in the historical emission order
|
|
112
|
-
# (always-truthy → unreachable-clause → shadowed-rescue → ivar-write →
|
|
113
|
-
# duplicate-hash-key → return-in-ensure). Keys match {.build_node_collectors}'
|
|
114
|
-
# `(path, results)` module_function on this module.
|
|
111
|
+
# (value-use-void → always-truthy → unreachable-clause → shadowed-rescue → ivar-write →
|
|
112
|
+
# dead-assignment → duplicate-hash-key → return-in-ensure). Keys match {.build_node_collectors}'
|
|
113
|
+
# roles; each value is a `(path, results)` module_function on this module. `void_value_use` leads
|
|
114
|
+
# because its standalone walk used to run before this fold, and the emission order is byte-identical
|
|
115
|
+
# output, not an implementation detail.
|
|
115
116
|
COLLECTOR_DIAGNOSTIC_BUILDERS = {
|
|
117
|
+
void_value_use: :void_value_use_diagnostics,
|
|
116
118
|
always_truthy: :always_truthy_condition_diagnostics,
|
|
117
119
|
unreachable_clauses: :unreachable_clause_diagnostics,
|
|
118
120
|
shadowed_rescues: :shadowed_rescue_diagnostics,
|
|
@@ -155,6 +157,7 @@ module Rigor
|
|
|
155
157
|
def build_node_collectors(path, scope_index)
|
|
156
158
|
{
|
|
157
159
|
main_pass: MainPassCollector.new(->(node) { main_pass_node_diagnostics(path, node, scope_index) }),
|
|
160
|
+
void_value_use: VoidValueUseCollector.new(scope_index),
|
|
158
161
|
always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
|
|
159
162
|
unreachable_clauses: UnreachableClauseCollector.new(scope_index),
|
|
160
163
|
shadowed_rescues: ShadowedRescueCollector.new(scope_index),
|
|
@@ -354,12 +357,11 @@ module Rigor
|
|
|
354
357
|
end
|
|
355
358
|
end
|
|
356
359
|
|
|
357
|
-
# ADR-100 WD2 — `static.value-use.void`.
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
VoidValueUseCollector.new(scope_index).collect(root).map do |result|
|
|
360
|
+
# ADR-100 WD2 — `static.value-use.void`. Its value-context slot inspection rides the shared per-node
|
|
361
|
+
# {RuleWalk} like the other collectors, so the file is not re-traversed for it. Each result is a
|
|
362
|
+
# value-context use of a call whose author-declared `-> void` return the engine recovered to `top`.
|
|
363
|
+
def void_value_use_diagnostics(path, results)
|
|
364
|
+
results.map do |result|
|
|
363
365
|
build_void_value_use_diagnostic(path, result)
|
|
364
366
|
end
|
|
365
367
|
end
|
|
@@ -1269,13 +1271,23 @@ module Rigor
|
|
|
1269
1271
|
# presence checks below cannot rule out a sound call.
|
|
1270
1272
|
return nil unless Rigor::Reflection.rbs_class_known?("NilClass", scope: scope)
|
|
1271
1273
|
|
|
1272
|
-
return nil unless
|
|
1273
|
-
return nil unless union_method_present_on_non_nil?(receiver_type, call_node.name, scope)
|
|
1274
|
-
return nil if nil_class_has_method?(call_node.name, scope)
|
|
1274
|
+
return nil unless nil_bearing_union_witnesses?(receiver_type, call_node.name, scope)
|
|
1275
1275
|
|
|
1276
1276
|
build_nil_receiver_diagnostic(path, call_node)
|
|
1277
1277
|
end
|
|
1278
1278
|
|
|
1279
|
+
# The receiver-type half of the rule, factored out of the node-shape
|
|
1280
|
+
# guards above: the union must carry nil, must carry a non-nil arm the
|
|
1281
|
+
# presence question can be asked of, must support the method on every
|
|
1282
|
+
# non-nil arm (so the call is only wrong on the nil path), and the
|
|
1283
|
+
# method must be absent from `NilClass` (so the nil path really raises).
|
|
1284
|
+
def nil_bearing_union_witnesses?(receiver_type, method_name, scope)
|
|
1285
|
+
union_contains_nil?(receiver_type) &&
|
|
1286
|
+
union_has_nameable_non_nil_arm?(receiver_type) &&
|
|
1287
|
+
union_method_present_on_non_nil?(receiver_type, method_name, scope) &&
|
|
1288
|
+
!nil_class_has_method?(method_name, scope)
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1279
1291
|
def union_contains_nil?(union)
|
|
1280
1292
|
union.members.any? { |member| nil_member?(member) }
|
|
1281
1293
|
end
|
|
@@ -1300,6 +1312,23 @@ module Rigor
|
|
|
1300
1312
|
(member.is_a?(Type::Nominal) && member.class_name == "NilClass")
|
|
1301
1313
|
end
|
|
1302
1314
|
|
|
1315
|
+
# Possible-nil may witness only where the presence question below is
|
|
1316
|
+
# ANSWERABLE. `method_present_anywhere?` reports "present" for a
|
|
1317
|
+
# nameless arm (Dynamic / Top / Bot) — the permissive polarity the
|
|
1318
|
+
# union-undefined-method rule's FP safety rests on — so a union whose
|
|
1319
|
+
# non-nil arms are ALL nameless satisfied that gate vacuously and fired
|
|
1320
|
+
# on every method name, including names defined on no class anywhere.
|
|
1321
|
+
# That inverted the intent: the gate suppressed exactly where knowledge
|
|
1322
|
+
# exists (`String | nil` calling a nonexistent method stays silent) and
|
|
1323
|
+
# permitted exactly where none does. Requiring one nameable concrete
|
|
1324
|
+
# arm restores the polarity without touching the shared helper:
|
|
1325
|
+
# `String | nil` keeps firing, and so does `Dynamic | String | nil` —
|
|
1326
|
+
# the nameless arm stays permissive inside the all-arms check, which is
|
|
1327
|
+
# only about the arms' method surface.
|
|
1328
|
+
def union_has_nameable_non_nil_arm?(union)
|
|
1329
|
+
union.members.any? { |m| !nil_member?(m) && !concrete_class_name(m).nil? }
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1303
1332
|
# The non-nil members must collectively support the
|
|
1304
1333
|
# method (i.e. for every non-nil member, the method
|
|
1305
1334
|
# exists on its class via RBS or in-source discovery).
|
|
@@ -2073,13 +2102,25 @@ module Rigor
|
|
|
2073
2102
|
param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
|
|
2074
2103
|
mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
|
|
2075
2104
|
return nil if mismatch.nil?
|
|
2076
|
-
|
|
2077
|
-
# open-call-site lower bound; firing argument-type-mismatch against it is an FP by construction.
|
|
2078
|
-
return nil if inferred_param_argument?(mismatch[:node], scope)
|
|
2105
|
+
return nil if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
|
|
2079
2106
|
|
|
2080
2107
|
build_argument_type_diagnostic(path, call_node, class_name, mismatch)
|
|
2081
2108
|
end
|
|
2082
2109
|
|
|
2110
|
+
# ADR-67 WD6b — an argument-type-mismatch verdict resting on an open-call-site lower bound, on
|
|
2111
|
+
# either side of the call. The ARGUMENT side: the mismatching argument is an inferred-parameter
|
|
2112
|
+
# local, so firing against it is an FP by construction. The RECEIVER side: when the receiver roots
|
|
2113
|
+
# at an inferred parameter, the method whose parameter contract the argument was checked against
|
|
2114
|
+
# was itself resolved through a lower-bound type, so the whole verdict is speculative. The 2026-07-30
|
|
2115
|
+
# self-check surfaced the receiver half as a guard hole: seeding `env : RBS::Environment` activated
|
|
2116
|
+
# this rule on `env.unload(culprits)` and flagged a correct Array argument against `unload`'s
|
|
2117
|
+
# declared `Set[Pathname]` — an upstream signature stricter than its implementation, exactly the FP
|
|
2118
|
+
# class WD6b exists to suppress. The other guarded rules already declined on a param-rooted
|
|
2119
|
+
# receiver; this brings argument-type-mismatch in line.
|
|
2120
|
+
def inferred_param_mismatch_verdict?(call_node, mismatch, scope)
|
|
2121
|
+
inferred_param_argument?(mismatch[:node], scope) || inferred_param_receiver?(call_node, scope)
|
|
2122
|
+
end
|
|
2123
|
+
|
|
2083
2124
|
# Single overload → the exact per-argument acceptance (unchanged).
|
|
2084
2125
|
# Multiple overloads → the nil channel (a pure-`nil` argument every
|
|
2085
2126
|
# overload rejects) plus, on non-coerce methods, the non-nil channel
|
|
@@ -2117,7 +2158,7 @@ module Rigor
|
|
|
2117
2158
|
arguments = call_node.arguments&.arguments || []
|
|
2118
2159
|
arguments.each_with_index do |arg, index|
|
|
2119
2160
|
arg_type = scope.type_of(arg)
|
|
2120
|
-
params = overload_positional_params(
|
|
2161
|
+
params = overload_positional_params(method_types, index)
|
|
2121
2162
|
next if params.nil? # arity divergence — some overload lacks a param here
|
|
2122
2163
|
|
|
2123
2164
|
mismatch =
|
|
@@ -2152,11 +2193,30 @@ module Rigor
|
|
|
2152
2193
|
# The matching positional RBS param across every overload, or nil when
|
|
2153
2194
|
# any overload has no param at `index` (arity divergence — the
|
|
2154
2195
|
# wrong-arity rule's concern, not this one's).
|
|
2155
|
-
def overload_positional_params(
|
|
2156
|
-
params =
|
|
2196
|
+
def overload_positional_params(method_types, index)
|
|
2197
|
+
params = method_types.map do |method_type|
|
|
2198
|
+
function = method_type.type
|
|
2199
|
+
param = (function.required_positionals + function.optional_positionals)[index]
|
|
2200
|
+
param && resolve_param_bounds(param, method_type)
|
|
2201
|
+
end
|
|
2157
2202
|
params.any?(&:nil?) ? nil : params
|
|
2158
2203
|
end
|
|
2159
2204
|
|
|
2205
|
+
# Substitutes each bounded method-level type parameter for its bound, so
|
|
2206
|
+
# `[I < _ToInt] (I index) -> …` is walked as `(_ToInt index) -> …`. A bare
|
|
2207
|
+
# `Variable` is undecidable to the acceptance walk and admits everything,
|
|
2208
|
+
# which silently disables both channels for the whole overload; the bound
|
|
2209
|
+
# constrains the argument exactly as an ordinary param of that type would.
|
|
2210
|
+
# Load-bearing since rbs 4.1 rewrote core signatures into this form
|
|
2211
|
+
# (`Array#fetch`'s block overload is `[I < _ToInt, T] (I index) { … }`).
|
|
2212
|
+
def resolve_param_bounds(param, method_type)
|
|
2213
|
+
bounded = method_type.type_params.select(&:upper_bound)
|
|
2214
|
+
return param if bounded.empty?
|
|
2215
|
+
|
|
2216
|
+
substitution = RBS::Substitution.build(bounded.map(&:name), bounded.map(&:upper_bound))
|
|
2217
|
+
param.map_type { |type| type.sub(substitution) }
|
|
2218
|
+
end
|
|
2219
|
+
|
|
2160
2220
|
# The class names whose instances `nil` IS — `NilClass` and every
|
|
2161
2221
|
# ancestor. A parameter typed as any other class instance rejects nil.
|
|
2162
2222
|
NIL_COMPATIBLE_CLASS_NAMES = %w[NilClass Object BasicObject Kernel].to_set.freeze
|