rigortype 0.3.7 → 0.3.8
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 +1 -1
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +7 -0
- data/docs/manual/04-diagnostics.md +1 -1
- data/lib/rigor/analysis/crash_signature.rb +78 -6
- data/lib/rigor/analysis/incremental_session.rb +19 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +142 -17
- data/lib/rigor/analysis/runner/pool_coordinator.rb +232 -47
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +66 -7
- data/lib/rigor/analysis/worker_session.rb +28 -3
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +39 -0
- data/lib/rigor/cache/store.rb +8 -1
- data/lib/rigor/cli/sig_gen_command.rb +29 -0
- data/lib/rigor/environment/failure_slot.rb +28 -0
- data/lib/rigor/environment/rbs_loader.rb +228 -57
- data/lib/rigor/environment.rb +87 -10
- data/lib/rigor/inference/hkt_registry.rb +16 -4
- data/lib/rigor/inference/hkt_sugar_translator.rb +15 -21
- data/lib/rigor/plugin/registry.rb +25 -2
- data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- data/lib/rigor/sig_gen/generator.rb +66 -80
- data/lib/rigor/sig_gen/renderer.rb +13 -6
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +6 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 30b66e3d183999cbd85557c5af6b770ac78bf82507a2e587e5878c1a411b4326
|
|
4
|
+
data.tar.gz: 2c3cee80d75ea9769d7b965dd84ac60eb4af89c27fc63d3ec35a1cdce3b45338
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9986a78b908e05fd017f772b0c26d6bc3d654e665cbf59d85cdf0176b76735403d644238ca7072268f9e47ba3937073002de47a308deda6cab77311459f6f008
|
|
7
|
+
data.tar.gz: 66df68005471dc8df33c9a1881659644fca80c4e406830b7ab9c1fe5b641e3872d13a70ab39cccdf66fba3d256e46026ab13963a70a76be1362ac86b8e823f1d
|
data/README.md
CHANGED
|
@@ -231,7 +231,7 @@ rigor docs --list # list every bundled page
|
|
|
231
231
|
|
|
232
232
|
## Status
|
|
233
233
|
|
|
234
|
-
Current release: **`v0.3.
|
|
234
|
+
Current release: **`v0.3.8`** (2026-09-08) — on the
|
|
235
235
|
`0.3.x` line, continuing the evaluation era opened by `v0.2.0`, the first
|
|
236
236
|
publicly-announced (general / evaluation) release. The line publishes
|
|
237
237
|
an enumerated [compatibility surface](docs/compatibility.md) as a
|
data/docs/handbook/11-sig-gen.md
CHANGED
|
@@ -95,10 +95,13 @@ states:
|
|
|
95
95
|
|
|
96
96
|
The `sig.skipped.*` reasons are:
|
|
97
97
|
|
|
98
|
-
- `sig.skipped.complex-shape` —
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
- `sig.skipped.complex-shape` — reserved for a parameter
|
|
99
|
+
shape the renderer cannot spell. Every shape a `def` can
|
|
100
|
+
declare renders today (optional, rest, trailing, keyword,
|
|
101
|
+
keyword-rest, `...` forwarding, `&block`), so the
|
|
102
|
+
generator does not produce this reason; it stayed
|
|
103
|
+
reserved when the gate that used to fire it for every
|
|
104
|
+
such method was retired (#778).
|
|
102
105
|
- `sig.skipped.untyped-return` — the method body's last
|
|
103
106
|
expression types as `Dynamic[top]`. Emitting `untyped` as
|
|
104
107
|
a tightening would be noise rather than help.
|
|
@@ -119,16 +122,25 @@ The `sig.skipped.*` reasons are:
|
|
|
119
122
|
The three `sig.generated.*` identifiers
|
|
120
123
|
(`sig.generated.new-file` / `new-method` / `tighter-return`)
|
|
121
124
|
are emitted as JSON fields under `--format=json` so CI
|
|
122
|
-
gating consumers can route them.
|
|
125
|
+
gating consumers can route them. Every `skipped` row is part
|
|
126
|
+
of the same payload, carrying its `sig.skipped.*` identifier
|
|
127
|
+
as `skip_reason`, so a method missing from your `sig/` has
|
|
128
|
+
its reason next to the rows that did emit. In text mode a
|
|
129
|
+
one-line stderr summary counts the skipped methods per
|
|
130
|
+
reason instead; stdout stays paste-clean.
|
|
123
131
|
|
|
124
132
|
## What method shapes the generator covers
|
|
125
133
|
|
|
126
134
|
Slice-by-slice (each shipped via a CHANGELOG entry — this
|
|
127
135
|
list is the current state):
|
|
128
136
|
|
|
129
|
-
- **Plain instance `def foo`**
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
- **Plain instance `def foo`** of any parameter shape:
|
|
138
|
+
required, optional, rest, trailing, keyword, keyword-rest,
|
|
139
|
+
`...` forwarding and `&block`. The parameter list mirrors
|
|
140
|
+
the runtime shape with `untyped` in every position (the
|
|
141
|
+
observed union under `--params=observed`), and a block
|
|
142
|
+
renders as `?{ (*untyped) -> untyped }`. Both new-method
|
|
143
|
+
and tighter-return paths apply.
|
|
132
144
|
- **Singleton-side `def self.foo`** and
|
|
133
145
|
`class << self; def foo; end`. Rendered as
|
|
134
146
|
`def self.foo: ...`; matched against
|
|
@@ -143,10 +155,8 @@ list is the current state):
|
|
|
143
155
|
are recognised as user-authored and never produce a
|
|
144
156
|
duplicate `def` insertion.
|
|
145
157
|
|
|
146
|
-
Method shapes the generator does **not** cover yet
|
|
147
|
-
silently skips):
|
|
158
|
+
Method shapes the generator does **not** cover yet:
|
|
148
159
|
|
|
149
|
-
- Optional / rest / keyword / block / forwarding parameters.
|
|
150
160
|
- `define_method(:name) { ... }`.
|
|
151
161
|
- Methods whose body types as `Dynamic[top]` (the body
|
|
152
162
|
inference cannot prove a useful return type).
|
|
@@ -303,9 +313,9 @@ inference, not a separate analysis.
|
|
|
303
313
|
|
|
304
314
|
## Limits today
|
|
305
315
|
|
|
306
|
-
-
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
- A block parameter always renders as the lenient
|
|
317
|
+
`?{ (*untyped) -> untyped }`; a typed block signature
|
|
318
|
+
waits on the engine tracking yield shapes end-to-end.
|
|
309
319
|
- `define_method` and `Data.define`-specific emission are
|
|
310
320
|
deferred follow-ups (`Data.define`-derived readers come
|
|
311
321
|
through if a method body exists).
|
|
@@ -475,6 +475,13 @@ exits `1`: you asked for a write and did not get one. Such a skip
|
|
|
475
475
|
is a bug in Rigor's RBS rendering, not in your code — please
|
|
476
476
|
report it.
|
|
477
477
|
|
|
478
|
+
A method the generator declined for any other reason (a body
|
|
479
|
+
that proves only `untyped`, a user-authored declaration it will
|
|
480
|
+
not overwrite) is never a silent absence: under `--format=json`
|
|
481
|
+
it is a `skipped` row of the `candidates` array with its
|
|
482
|
+
`sig.skipped.*` identifier in `skip_reason`, and in text mode a
|
|
483
|
+
one-line stderr summary counts the skipped methods per reason.
|
|
484
|
+
|
|
478
485
|
## `rigor lsp`
|
|
479
486
|
|
|
480
487
|
Run the Language Server over stdio. See
|
|
@@ -74,7 +74,7 @@ carries no `documentation_url`.
|
|
|
74
74
|
| <a id="rule-effect-envelope-exceeded"></a>`effect.envelope-exceeded` | A method performs an effect its declared envelope does not admit — its proven effect labels (its own body plus everything it calls) are not covered by the `%a{pure}` or `%a{rigor:v1:effect …}` bound written on it or on its class. Opt-in twice over: it needs an `effects:` block in `.rigor.yml` and an envelope you wrote. Positioned at the Ruby `def`. Unproven ("and possibly more") effects never fire, and `mutate.local` is tolerated by every envelope. | high |
|
|
75
75
|
| <a id="rule-effect-liskov-widened"></a>`effect.liskov-widened` | An override escapes the envelope written on the method it overrides. A `PgRepo` is usable wherever a `Repo` is, so a `%a{rigor:v1:effect io.db}` on `Repo#find` binds `PgRepo#find` too: an implementation may be purer than the bound it inherits, never less pure. Either what the override *does* exceeds the inherited bound, or the envelope the override *declares for itself* is wider than it. Both sides must be authored — nothing fires unless someone wrote an envelope on the ancestor — and only subclassing counts, not `include`. Positioned at the override's `def`. Needs an `effects:` block. | high |
|
|
76
76
|
| <a id="rule-effect-unknown-label"></a>`effect.unknown-label` | An effect declaration names a label the registry does not know — a typo in an envelope (`%a{rigor:v1:effect io.bd}`), or a member of `effects.tolerated:`. The whole tag then reads as unbounded, so the declaration quietly stops doing anything; this says so. Positioned at the declaration: the `.rbs` line, the `.rb` line for an rbs-inline annotation, or `.rigor.yml` for a config value. `# rigor:disable` comments are not read out of `.rbs` or `.rigor.yml`, so use `disable:` or the baseline there. Only fires where the spelling is evidently meant to be a label (close to a known one, next to a known one, dotted, or retired) — a word nothing resembles stays silent, because you may be opening your own vocabulary. Needs an `effects:` block. | high |
|
|
77
|
-
| <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments —
|
|
77
|
+
| <a id="rule-effect-annotations-unchecked"></a>`effect.annotations-unchecked` | Your signatures carry `%a{pure}` / `%a{rigor:v1:effect …}` but `.rigor.yml` has no `effects:` block, so nothing checks them. One `:info` per run, positioned at the first annotation. An annotation never turns effect collection on by itself — that would make one line in one file more expensive for every run — so this is how it tells you instead. Add `effects: {}` to opt in, or `disable:` it to keep the annotations documentary. Reads both annotation lanes — `sig/*.rbs` and rbs-inline comments — on every run, a warm `--incremental` with nothing changed included. | — |
|
|
78
78
|
| <a id="rule-suppression-unknown-rule"></a>`suppression.unknown-rule` | A `# rigor:disable[-file]` comment names a rule that does not exist (typically a typo), so the suppression silently does nothing. `plugin.`-prefixed tokens are never flagged. | high |
|
|
79
79
|
| <a id="rule-suppression-empty"></a>`suppression.empty` | A `# rigor:disable[-file]` comment lists no rules, so it suppresses nothing. | high |
|
|
80
80
|
| <a id="rule-suppression-unknown-marker"></a>`suppression.unknown-marker` | A comment uses a suppression marker Rigor does not recognise — typically the RuboCop reflex `# rigor:disable-next-line <rule>` or `# rigor:enable <rule>`. Rigor's only markers are `# rigor:disable <rules>` (suppresses on its own line) and `# rigor:disable-file <rules>`, so the comment suppresses nothing. | high |
|
|
@@ -42,9 +42,16 @@ module Rigor
|
|
|
42
42
|
# prepare row is appended to every sequential run, it would refuse every run for the life of the
|
|
43
43
|
# process.
|
|
44
44
|
# - `:rbs_build` — the analysis ran to completion and every rule fired, over a type universe missing one
|
|
45
|
-
# class (`rbs.coverage.definition-build-failed`)
|
|
46
|
-
# (`rbs.coverage.environment-build-failed`)
|
|
47
|
-
#
|
|
45
|
+
# class (`rbs.coverage.definition-build-failed`), all of them
|
|
46
|
+
# (`rbs.coverage.environment-build-failed`), or only the HKT registrations the `type` aliases and the
|
|
47
|
+
# loaded plugins' manifests would have contributed (`rbs.coverage.hkt-scan-failed`, issues #784 and
|
|
48
|
+
# #791). The first two are degradations
|
|
49
|
+
# the user causes and the diagnostic itself reports; a project can sit on one for a release while it
|
|
50
|
+
# fixes its `sig/`. The third is NOT the user's: post-#783 the scan raising is an analyzer defect, and
|
|
51
|
+
# the overlay stage is a defect in Rigor or in a plugin the user installed, never in their `sig/`. It
|
|
52
|
+
# still belongs here rather than under `:check_rule`, because the question this classification
|
|
53
|
+
# answers is "may a consumer still read the run's diagnostics?" — and it may: every rule fired. What
|
|
54
|
+
# differs is whether the run is a valid measurement OF RIGOR, which is {.analyzer_defect?}'s question.
|
|
48
55
|
#
|
|
49
56
|
# The two consumer tiers therefore differ on purpose. The ADR-69 kill oracles arm `:check_rule` only:
|
|
50
57
|
# refusing a run they could still have measured is the same "manufactures work" error as scoring an
|
|
@@ -63,10 +70,11 @@ module Rigor
|
|
|
63
70
|
PLUGIN_SOURCE_FAMILY = :plugin_loader
|
|
64
71
|
PLUGIN_RULE = "runtime-error"
|
|
65
72
|
|
|
66
|
-
# The `rbs.coverage.*` rules that mean declared types went missing from this run —
|
|
67
|
-
#
|
|
68
|
-
# in `docs/type-specification/diagnostic-policy.md`.
|
|
73
|
+
# The `rbs.coverage.*` rules that mean declared types went missing from this run — the implicit-HKT
|
|
74
|
+
# scan over `type` aliases (#784), a per-class definition build (#696), and the env-wide collapse.
|
|
75
|
+
# Ordered widest consequence last, the way the rows sit in `docs/type-specification/diagnostic-policy.md`.
|
|
69
76
|
RBS_BUILD_FAILURE_RULES = %w[
|
|
77
|
+
rbs.coverage.hkt-scan-failed
|
|
70
78
|
rbs.coverage.definition-build-failed
|
|
71
79
|
rbs.coverage.environment-build-failed
|
|
72
80
|
].freeze
|
|
@@ -75,8 +83,62 @@ module Rigor
|
|
|
75
83
|
# `:rbs_build` both leave a readable run behind — see the class doc.
|
|
76
84
|
DISCARDS_FILE_ANALYSIS_REASON = :check_rule
|
|
77
85
|
|
|
86
|
+
# The `:rbs_build` rules whose cause is Rigor, not the user's `sig/` (issue #784). Readable for the
|
|
87
|
+
# user — every rule fired — but NOT a valid measurement of Rigor itself, so a harness that scores
|
|
88
|
+
# Rigor's behaviour should treat one of these as a crash finding the way it treats `:check_rule`, or
|
|
89
|
+
# a mutant that re-breaks the HKT scan (#776 was one) scores as a measurement over a silently degraded
|
|
90
|
+
# universe. What consults it today: `tool/mutation`'s fuzz crash detector — on the severity-resolved
|
|
91
|
+
# stream, so a `severity_overrides: rbs: off` hides the row from it. What does NOT yet: the ADR-69
|
|
92
|
+
# kill oracle (`Protection::AnalysisGuard` reads `Result#crashed?`, which excludes every
|
|
93
|
+
# `:rbs_build` row), and both harnesses still reuse one Environment across mutants, which memoises
|
|
94
|
+
# the degraded registry after the first defect. Arming the oracle, inspecting the pre-severity row,
|
|
95
|
+
# and resetting the Environment are issue #790.
|
|
96
|
+
ANALYZER_DEFECT_RULES = %w[
|
|
97
|
+
rbs.coverage.hkt-scan-failed
|
|
98
|
+
].freeze
|
|
99
|
+
|
|
78
100
|
module_function
|
|
79
101
|
|
|
102
|
+
# The message `Runner#analyze_file_body` / `WorkerSession#analyze_body` fold a raised `StandardError`
|
|
103
|
+
# into. Built here, not at either rescue site, so the two twins cannot drift (issue #665) — and so
|
|
104
|
+
# the appended crash frame is derived identically on the sequential and pooled paths. Keeps the
|
|
105
|
+
# {CHECK_RULE_MESSAGE_PREFIX} prefix every consumer matches on; the frame is a trailing hint.
|
|
106
|
+
#
|
|
107
|
+
# @param error [StandardError]
|
|
108
|
+
# @return [String]
|
|
109
|
+
def check_rule_message(error)
|
|
110
|
+
base = "#{CHECK_RULE_MESSAGE_PREFIX}: #{error.class}: #{error.message}"
|
|
111
|
+
frame = crash_frame(error)
|
|
112
|
+
frame ? "#{base} (#{frame})" : base
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The first `lib/rigor/` backtrace frame — the raise site, path made repo-relative so it reads the
|
|
116
|
+
# same whether Rigor runs from a checkout or an installed gem. A LOCATION, not an attribution: a
|
|
117
|
+
# bundled or third-party plugin also lives under `lib/rigor/<plugin>/`, so a frame here says where
|
|
118
|
+
# the raise was, never whose defect it is. Falls back to the raw top frame when the crash is entirely
|
|
119
|
+
# inside a dependency, and to nil when there is no backtrace at all.
|
|
120
|
+
#
|
|
121
|
+
# @param error [Exception]
|
|
122
|
+
# @return [String, nil]
|
|
123
|
+
def crash_frame(error)
|
|
124
|
+
frames = error.backtrace
|
|
125
|
+
return nil if frames.nil? || frames.empty?
|
|
126
|
+
|
|
127
|
+
relativize_frame(frames.find { |f| f.include?("/lib/rigor/") } || frames.first)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The repo-relative half of {.crash_frame}, pulled out on its own so a second caller — the #784
|
|
131
|
+
# `rbs.coverage.hkt-scan-failed` diagnostic, which stores its raw frame across a `Marshal` boundary
|
|
132
|
+
# (the fork pool) rather than deriving it fresh from a live `Exception` — can relativize the frame it
|
|
133
|
+
# already has without re-deriving `.crash_frame`'s "which frame" choice. Nil-safe, and a no-op (`sub`
|
|
134
|
+
# never matches) on a frame with no `lib/rigor/` segment at all.
|
|
135
|
+
#
|
|
136
|
+
# @param frame [String, nil]
|
|
137
|
+
# @return [String, nil]
|
|
138
|
+
def relativize_frame(frame)
|
|
139
|
+
frame&.sub(%r{\A.*/(lib/rigor/)}, '\1')
|
|
140
|
+
end
|
|
141
|
+
|
|
80
142
|
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
81
143
|
# @return [Symbol, nil] `:check_rule`, `:plugin`, `:rbs_build`, or nil for an ordinary diagnostic.
|
|
82
144
|
def reason(diagnostic)
|
|
@@ -96,6 +158,16 @@ module Rigor
|
|
|
96
158
|
reason(diagnostic) == DISCARDS_FILE_ANALYSIS_REASON
|
|
97
159
|
end
|
|
98
160
|
|
|
161
|
+
# True when `diagnostic` reports a failure inside Rigor that left the run readable but invalid as a
|
|
162
|
+
# measurement of Rigor — see {ANALYZER_DEFECT_RULES}. Orthogonal to {.discards_file_analysis?}: the
|
|
163
|
+
# user-facing tier reads the run; the Rigor-measuring tier should refuse it (the mutation fuzz does,
|
|
164
|
+
# the ADR-69 kill oracle does not yet — #790).
|
|
165
|
+
#
|
|
166
|
+
# @param diagnostic [Rigor::Analysis::Diagnostic]
|
|
167
|
+
def analyzer_defect?(diagnostic)
|
|
168
|
+
ANALYZER_DEFECT_RULES.include?(diagnostic.rule)
|
|
169
|
+
end
|
|
170
|
+
|
|
99
171
|
# A one-line "<reason> at <path>:<line>: <message>" for a failure message, so whoever reads the raise
|
|
100
172
|
# sees which shape fired and where without re-deriving it.
|
|
101
173
|
#
|
|
@@ -172,7 +172,7 @@ module Rigor
|
|
|
172
172
|
# ADR-103 WD13 / #382 — a baseline collects every file, so its collections are the whole world.
|
|
173
173
|
@effect_collections = runner.effect_collections_by_path
|
|
174
174
|
@effects_identity = current_effects_identity
|
|
175
|
-
@cache = per_file(
|
|
175
|
+
@cache = per_file(runner.per_file_diagnostics)
|
|
176
176
|
@digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
|
|
177
177
|
diagnostics
|
|
178
178
|
end
|
|
@@ -202,7 +202,7 @@ module Rigor
|
|
|
202
202
|
@last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
|
|
203
203
|
reused = (current & previous) - affected.to_a
|
|
204
204
|
merged = fresh + reused.flat_map { |path| @cache[path] || [] }
|
|
205
|
-
absorb(runner,
|
|
205
|
+
absorb(runner, current, analyze_set, removed)
|
|
206
206
|
@param_table = fresh_params
|
|
207
207
|
Recheck.new(diagnostics: merged, changed: changed.to_set, added: added.to_set,
|
|
208
208
|
removed: removed.to_set, affected: affected, reused: reused.to_set)
|
|
@@ -684,13 +684,13 @@ module Rigor
|
|
|
684
684
|
# Fold a #recheck's fresh results back into the cache + graph so the session is correct across
|
|
685
685
|
# multiple edits: the analyzed set gets fresh diagnostics + digests + dependency edges, removed files
|
|
686
686
|
# are evicted from every map, and the analyzed-file list advances to `current`.
|
|
687
|
-
def absorb(runner,
|
|
687
|
+
def absorb(runner, current, analyze_set, removed)
|
|
688
688
|
removed.each { |path| forget(path) }
|
|
689
689
|
@analyzed = current
|
|
690
690
|
# ADR-85 WD2 — the recheck's discovery folded the restored bundles and refreshed them (changed files
|
|
691
691
|
# re-walked, removed files dropped, added files built), so adopt the runner's current set wholesale.
|
|
692
692
|
@seed_bundles = runner.seed_bundles
|
|
693
|
-
fresh_by_file = per_file(
|
|
693
|
+
fresh_by_file = per_file(runner.per_file_diagnostics)
|
|
694
694
|
analyze_set.each do |path|
|
|
695
695
|
@cache[path] = fresh_by_file[path] || []
|
|
696
696
|
@digests[path] = pack_digest(path)
|
|
@@ -984,9 +984,21 @@ module Rigor
|
|
|
984
984
|
@paths ? runner.run(@paths) : runner.run
|
|
985
985
|
end
|
|
986
986
|
|
|
987
|
-
# Group diagnostics by
|
|
988
|
-
#
|
|
989
|
-
#
|
|
987
|
+
# Group the runner's PER-FILE diagnostics by path, keeping only analyzed project files.
|
|
988
|
+
#
|
|
989
|
+
# The argument is `Runner#per_file_diagnostics` — what `PoolCoordinator#analyze_files` returned, already
|
|
990
|
+
# severity-resolved as the run's stream is (this cache serves reused files without re-stamping, so a
|
|
991
|
+
# raw row would resurrect an `:off` rule and lose every override) — never the run's full stream. Every
|
|
992
|
+
# run-level row is regenerated by every run and must not be served
|
|
993
|
+
# from this cache, and "run-level" is a property of where a row was PRODUCED, not of where it is
|
|
994
|
+
# positioned: the `.rigor.yml` rows the slice below drops are the common case, but
|
|
995
|
+
# `effect.annotations-unchecked` sits at the first annotated file and the two `source-rbs-*` rows sit
|
|
996
|
+
# at the source file that carried the annotation, and all three are produced once per run off the
|
|
997
|
+
# environment and its reporters. Slicing the full stream cached them, and once a narrowed run's
|
|
998
|
+
# environment carried the whole project's synthesized RBS (#793) each was regenerated for a file the
|
|
999
|
+
# recheck did not analyse and met its cached twin in the merge — two rows, `--verify-incremental` red,
|
|
1000
|
+
# on the default configuration with the bundled rbs-inline plugin (#788 rounds 5 and 6; a rule-id
|
|
1001
|
+
# denylist missed the second pair). Feeding only the per-file stream makes the split structural.
|
|
990
1002
|
def per_file(diagnostics)
|
|
991
1003
|
diagnostics.group_by(&:path).slice(*@analyzed)
|
|
992
1004
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../crash_signature"
|
|
3
4
|
require_relative "../diagnostic"
|
|
4
5
|
require_relative "../severity_stamp"
|
|
5
6
|
|
|
@@ -39,13 +40,18 @@ module Rigor
|
|
|
39
40
|
# @param definition_build_failures_snapshot [#call] issue #696 — reader returning the per-class
|
|
40
41
|
# `RBS::DefinitionBuilder` failures the run observed, as `[class_name, error_class, member,
|
|
41
42
|
# conflicting_buffer_names]` tuples. Empty for a healthy sig set.
|
|
43
|
+
# @param hkt_scan_failure_snapshot [#call] issue #784 — reader returning the `[error_class_name,
|
|
44
|
+
# first_message_line, raw_frame_or_nil, stage]` tuple whichever stage of the HKT-registry build
|
|
45
|
+
# raised, or nil when both built (or were never demanded). `stage` is `:scan` (the RBS `type`-alias
|
|
46
|
+
# scan) or `:overlay` (the plugin-manifest aggregation, #791), and picks the row's wording.
|
|
42
47
|
# @param conformance_results_snapshot [#call] reader.
|
|
43
48
|
def initialize(configuration:, rbs_extended_reporter:, boundary_cross_reporter:, # rubocop:disable Metrics/ParameterLists
|
|
44
49
|
source_rbs_synthesis_reporter:, plugin_registry:, dependency_source_index:,
|
|
45
50
|
pool_mode:, cached_plugin_prepare_diagnostics:,
|
|
46
51
|
pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
|
|
47
52
|
quarantined_signatures_snapshot:, env_build_failure_snapshot:,
|
|
48
|
-
definition_build_failures_snapshot:,
|
|
53
|
+
definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
|
|
54
|
+
conformance_results_snapshot:)
|
|
49
55
|
@configuration = configuration
|
|
50
56
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
51
57
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -59,6 +65,7 @@ module Rigor
|
|
|
59
65
|
@quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
|
|
60
66
|
@env_build_failure_snapshot_reader = env_build_failure_snapshot
|
|
61
67
|
@definition_build_failures_snapshot_reader = definition_build_failures_snapshot
|
|
68
|
+
@hkt_scan_failure_snapshot_reader = hkt_scan_failure_snapshot
|
|
62
69
|
@conformance_results_snapshot_reader = conformance_results_snapshot
|
|
63
70
|
end
|
|
64
71
|
|
|
@@ -315,9 +322,11 @@ module Rigor
|
|
|
315
322
|
# user knows their sig set is malformed (`rbs validate` rejects it) and can fix it at the source.
|
|
316
323
|
# Authored `:info`: the analysis already succeeded; this is advisory, never a gate. Empty for a
|
|
317
324
|
# well-formed sig set.
|
|
318
|
-
# An unparseable `.rbs` under `signature_paths:` is QUARANTINED so the
|
|
319
|
-
#
|
|
320
|
-
#
|
|
325
|
+
# An unparseable or declaration-colliding `.rbs` under `signature_paths:` is QUARANTINED so the
|
|
326
|
+
# rest of the env survives (PR #50; issue #777 extends this to class-vs-module / constant
|
|
327
|
+
# collisions against bundled RBS), which means the types it declares are silently absent — calls
|
|
328
|
+
# into them read `Dynamic[top]`, and the run gets *quieter*, not louder. The stderr banner alone
|
|
329
|
+
# never reached CI:
|
|
321
330
|
# it is not a diagnostic, so it is absent from `--format json` / SARIF / GitHub annotations / the LSP
|
|
322
331
|
# and cannot move the exit code. This puts it in the diagnostic stream where every channel sees it.
|
|
323
332
|
#
|
|
@@ -381,6 +390,25 @@ module Rigor
|
|
|
381
390
|
[build_rbs_definition_build_failed_diagnostic(failures)]
|
|
382
391
|
end
|
|
383
392
|
|
|
393
|
+
# Issue #784 — the fourth rung, narrowest consequence: one of the two builds behind
|
|
394
|
+
# `Environment#hkt_registry` raised instead of building. The `:scan` stage is the implicit HKT scan
|
|
395
|
+
# over RBS `type` aliases (ADR-20 WD2's `%a{rigor:v1:hkt_register / hkt_define}` overlay AND any
|
|
396
|
+
# recursive `type` alias in the project's own `.rbs` or an installed `rbs collection`); analysis
|
|
397
|
+
# proceeds over the PRE-scan registry — bundled builtins (`json::value`, …) plus the plugin overlay
|
|
398
|
+
# — so a `type` alias that would have registered as a type constructor reads its bound
|
|
399
|
+
# (`Dynamic[top]`) instead. The `:overlay` stage (#791) is the plugin-manifest aggregation, which
|
|
400
|
+
# sat ABOVE the seam until the run-owned demands made a raise there abort the run; it degrades one
|
|
401
|
+
# step further in and one step narrower — the plugin entries are dropped, the `.rbs` scan still
|
|
402
|
+
# runs. Either way everything else this run reports is unaffected: no class loses its method
|
|
403
|
+
# surface, no signature file is skipped, the environment builds. That is why this sits LAST on the
|
|
404
|
+
# ladder, after its two `rbs.coverage.*` siblings above.
|
|
405
|
+
def rbs_hkt_scan_failed_diagnostics
|
|
406
|
+
failure = hkt_scan_failure_snapshot
|
|
407
|
+
return [] if failure.nil?
|
|
408
|
+
|
|
409
|
+
[build_rbs_hkt_scan_failed_diagnostic(failure)]
|
|
410
|
+
end
|
|
411
|
+
|
|
384
412
|
def rbs_synthesized_namespace_diagnostics
|
|
385
413
|
synthesized = synthesized_namespaces_snapshot
|
|
386
414
|
return [] if synthesized.nil? || synthesized.empty?
|
|
@@ -458,11 +486,12 @@ module Rigor
|
|
|
458
486
|
path: ".rigor.yml",
|
|
459
487
|
line: 1,
|
|
460
488
|
column: 1,
|
|
461
|
-
message: "#{quarantined.size} RBS file(s) under `signature_paths:`
|
|
462
|
-
"
|
|
463
|
-
"loaded, but the types those files declare
|
|
464
|
-
"`Dynamic[top]`, so this run is quieter than it
|
|
465
|
-
"parse error(s)
|
|
489
|
+
message: "#{quarantined.size} RBS file(s) under `signature_paths:` were SKIPPED " \
|
|
490
|
+
"(unparseable, or duplicated against bundled RBS): #{sample.join(', ')}#{suffix}. " \
|
|
491
|
+
"The rest of your RBS environment still loaded, but the types those files declare " \
|
|
492
|
+
"are absent — calls into them read `Dynamic[top]`, so this run is quieter than it " \
|
|
493
|
+
"should be, not cleaner. Fix the parse error(s) or remove the conflicting " \
|
|
494
|
+
"declaration(s) (`rbs validate`) to restore that coverage.",
|
|
466
495
|
severity: :warning,
|
|
467
496
|
rule: "rbs.coverage.quarantined-signature",
|
|
468
497
|
source_family: :builtin
|
|
@@ -550,6 +579,66 @@ module Rigor
|
|
|
550
579
|
"#{sample.join(', ')}#{suffix}"
|
|
551
580
|
end
|
|
552
581
|
|
|
582
|
+
# Issue #784 — one `:error` row per run, unlike its two `:warning` `rbs.coverage.*` siblings above.
|
|
583
|
+
# Those two are typically a collision between the user's OWN `sig/` and Rigor's bundled RBS, so an
|
|
584
|
+
# `:error` default would let a Rigor release turn a green project red with zero user change (ADR-5
|
|
585
|
+
# / AGENTS.md § FP discipline) — the reason both stay `:warning` by default. This row has no such
|
|
586
|
+
# neighbour: post-#783 the scan itself raising is an ANALYZER defect, never something a user's
|
|
587
|
+
# `sig/` could trigger on its own, so there is no green project this could newly redden. And the
|
|
588
|
+
# run was already non-zero before this row existed — issue #784's seam is what stopped the raise
|
|
589
|
+
# from reaching every file as N identical `internal analyzer error` rows in the first place, and
|
|
590
|
+
# THAT per-file rescue is what `Result#success?` was already reading as a failure; this row only
|
|
591
|
+
# makes the reason legible.
|
|
592
|
+
#
|
|
593
|
+
# One row per RUN, not per file: the scan is one build over the whole `signature_paths:` overlay
|
|
594
|
+
# (memoised — see {Environment#hkt_registry}), so every file that would have demanded it hit the
|
|
595
|
+
# exact same failure, and a row per file would say the same thing N times.
|
|
596
|
+
#
|
|
597
|
+
# `:rbs_build` (via {CrashSignature::RBS_BUILD_FAILURE_RULES}), not `:check_rule`: the analysis ran
|
|
598
|
+
# to completion over a DEGRADED type universe — every rule still fired, unlike `:check_rule`'s
|
|
599
|
+
# whole-file replacement — so a consumer gating on {CrashSignature.discards_file_analysis?} must
|
|
600
|
+
# keep reading this run's diagnostics rather than refuse it as a crash.
|
|
601
|
+
def build_rbs_hkt_scan_failed_diagnostic(failure)
|
|
602
|
+
error_class, first_line, frame, stage = failure
|
|
603
|
+
relative_frame = CrashSignature.relativize_frame(frame)
|
|
604
|
+
frame_clause = relative_frame ? " at #{relative_frame}" : ""
|
|
605
|
+
raised = "(#{error_class}): #{first_line}#{frame_clause}."
|
|
606
|
+
Diagnostic.new(
|
|
607
|
+
path: ".rigor.yml",
|
|
608
|
+
line: 1,
|
|
609
|
+
column: 1,
|
|
610
|
+
message: stage == :overlay ? hkt_overlay_failed_message(raised) : hkt_scan_failed_message(raised),
|
|
611
|
+
severity: :error,
|
|
612
|
+
rule: "rbs.coverage.hkt-scan-failed",
|
|
613
|
+
source_family: :builtin
|
|
614
|
+
)
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def hkt_scan_failed_message(raised)
|
|
618
|
+
"The implicit HKT scan over RBS `type` aliases raised #{raised} Rigor fell back to the bundled " \
|
|
619
|
+
"and plugin HKT registrations, so a recursive `type` alias in your `.rbs` or an installed " \
|
|
620
|
+
"`rbs collection` no longer registers as a type constructor and reads its bound " \
|
|
621
|
+
"(`Dynamic[top]`) instead — this run is quieter than it should be, not cleaner. " \
|
|
622
|
+
"This is an analyzer defect, not a problem with your signatures; please report it " \
|
|
623
|
+
"with the message above."
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# Issue #791 — the overlay stage of the same build, worded for the plugin it came from. The scan
|
|
627
|
+
# wording would send a user to their `.rbs` for a defect that is not there: what failed is the
|
|
628
|
+
# aggregation of the loaded plugins' manifest-declared HKT entries, and the message names the
|
|
629
|
+
# plugin whenever the raise was attributable to one (`Plugin::Registry#hkt_overlay_registry` puts
|
|
630
|
+
# the id in the message it re-raises). The degradation is narrower than the scan's, so the fallback
|
|
631
|
+
# sentence differs too: only the plugin entries are missing, and the user's own `.rbs` scan still
|
|
632
|
+
# ran on top of the bundled registrations.
|
|
633
|
+
def hkt_overlay_failed_message(raised)
|
|
634
|
+
"Building the plugin HKT overlay raised #{raised} Rigor skipped every plugin-declared HKT " \
|
|
635
|
+
"registration and analysed with the bundled ones plus your own `.rbs` overlay, so a type " \
|
|
636
|
+
"constructor a plugin declares reads its bound (`Dynamic[top]`) instead — this run is " \
|
|
637
|
+
"quieter than it should be, not cleaner. This is a defect in the named plugin or in Rigor, " \
|
|
638
|
+
"not a problem with your signatures; report it with the message above, or remove the plugin " \
|
|
639
|
+
"from `plugins:` to analyse without it."
|
|
640
|
+
end
|
|
641
|
+
|
|
553
642
|
# The absolute path is what the loader records; the user thinks in project-relative terms.
|
|
554
643
|
def relative_signature_path(path)
|
|
555
644
|
root = "#{Dir.pwd}#{File::SEPARATOR}"
|
|
@@ -603,16 +692,19 @@ module Rigor
|
|
|
603
692
|
# enable.
|
|
604
693
|
# - `dynamic.shape.lossy-projection` for every shape-projection type function (`pick_of`, …) applied
|
|
605
694
|
# to a carrier that loses precision (anything other than `HashShape` / `Tuple`).
|
|
695
|
+
# - `dynamic.rbs-extended.hkt-directive-invalid` for every malformed ADR-20 `rigor:v1:hkt_register` /
|
|
696
|
+
# `rigor:v1:hkt_define` the directive parser declined (issue #785).
|
|
606
697
|
#
|
|
607
|
-
#
|
|
608
|
-
# column
|
|
609
|
-
#
|
|
698
|
+
# All three are authored `:info`; the severity profile re-stamps them per project taste. Every stream
|
|
699
|
+
# carries its `(path, line, column)` already flattened off the annotation's `RBS::Location`, because
|
|
700
|
+
# all three cross the pool drain channel (see {RbsExtended::Reporter}); an entry with no position
|
|
701
|
+
# falls back to `.rigor.yml`-style file-level attribution.
|
|
610
702
|
def rbs_extended_reporter_diagnostics
|
|
611
703
|
return [] if @rbs_extended_reporter.empty?
|
|
612
704
|
|
|
613
705
|
unresolved = @rbs_extended_reporter.unresolved_payloads.map do |entry|
|
|
614
|
-
|
|
615
|
-
entry
|
|
706
|
+
build_positioned_reporter_diagnostic(
|
|
707
|
+
entry,
|
|
616
708
|
rule: "dynamic.rbs-extended.unresolved",
|
|
617
709
|
message: "`RBS::Extended` directive payload could not be resolved: " \
|
|
618
710
|
"#{entry.payload.inspect}. Check for typos or enable a plugin " \
|
|
@@ -621,8 +713,8 @@ module Rigor
|
|
|
621
713
|
end
|
|
622
714
|
|
|
623
715
|
lossy = @rbs_extended_reporter.lossy_projections.map do |entry|
|
|
624
|
-
|
|
625
|
-
entry
|
|
716
|
+
build_positioned_reporter_diagnostic(
|
|
717
|
+
entry,
|
|
626
718
|
rule: "dynamic.shape.lossy-projection",
|
|
627
719
|
message: "Shape projection `#{entry.head}` applied to a carrier without a " \
|
|
628
720
|
"literal shape; the projection degrades to the input type. Author " \
|
|
@@ -630,7 +722,22 @@ module Rigor
|
|
|
630
722
|
)
|
|
631
723
|
end
|
|
632
724
|
|
|
633
|
-
unresolved + lossy
|
|
725
|
+
unresolved + lossy + hkt_directive_diagnostics
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
# Issue #785 — one row per declined HKT directive. The consequence sentence is the point: the parser
|
|
729
|
+
# is fail-soft, so nothing else in the run tells the author that the constructor they registered is
|
|
730
|
+
# not there and that every `App[…]` naming it silently reads its bound.
|
|
731
|
+
def hkt_directive_diagnostics
|
|
732
|
+
@rbs_extended_reporter.hkt_directive_errors.map do |entry|
|
|
733
|
+
build_positioned_reporter_diagnostic(
|
|
734
|
+
entry,
|
|
735
|
+
rule: "dynamic.rbs-extended.hkt-directive-invalid",
|
|
736
|
+
message: "`RBS::Extended` HKT directive was declined: #{entry.message}. The type " \
|
|
737
|
+
"constructor stays unregistered, so an `App[...]` carrier naming it reads " \
|
|
738
|
+
"its bound (`Dynamic[top]`) instead of the type function."
|
|
739
|
+
)
|
|
740
|
+
end
|
|
634
741
|
end
|
|
635
742
|
|
|
636
743
|
# ADR-32 WD6 — drains the per-run {Plugin::SourceRbsSynthesisReporter} into
|
|
@@ -712,6 +819,20 @@ module Rigor
|
|
|
712
819
|
)
|
|
713
820
|
end
|
|
714
821
|
|
|
822
|
+
# The {RbsExtended::Reporter} form of the builder above: its three streams carry the position as
|
|
823
|
+
# `(path, line, column)` primitives rather than as the `RBS::Location` a conformance record holds,
|
|
824
|
+
# because they cross the pool drain channel (#785, #805). A missing component falls back exactly as
|
|
825
|
+
# {#location_fields} does for a missing location.
|
|
826
|
+
def build_positioned_reporter_diagnostic(entry, rule:, message:)
|
|
827
|
+
path = entry.path.to_s
|
|
828
|
+
Diagnostic.new(
|
|
829
|
+
path: path.empty? ? ".rigor.yml" : path,
|
|
830
|
+
line: entry.line || 1,
|
|
831
|
+
column: entry.column || 1,
|
|
832
|
+
message: message, severity: :info, rule: rule, source_family: :builtin
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
|
|
715
836
|
def location_fields(source_location)
|
|
716
837
|
return [".rigor.yml", 1, 1] if source_location.nil?
|
|
717
838
|
|
|
@@ -776,6 +897,10 @@ module Rigor
|
|
|
776
897
|
@definition_build_failures_snapshot_reader.call
|
|
777
898
|
end
|
|
778
899
|
|
|
900
|
+
def hkt_scan_failure_snapshot
|
|
901
|
+
@hkt_scan_failure_snapshot_reader.call
|
|
902
|
+
end
|
|
903
|
+
|
|
779
904
|
def conformance_results_snapshot
|
|
780
905
|
@conformance_results_snapshot_reader.call
|
|
781
906
|
end
|