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
|
@@ -84,11 +84,14 @@ module Rigor
|
|
|
84
84
|
# sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
|
|
85
85
|
add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
|
|
86
86
|
env = RBS::Environment.from_loader(rbs_loader)
|
|
87
|
+
project_files = project_sig_files(signature_paths)
|
|
87
88
|
add_project_signatures(env, signature_paths, deferred_signature_paths)
|
|
88
89
|
add_virtual_rbs(env, virtual_rbs)
|
|
89
90
|
synthesize_missing_namespaces(env)
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
# Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
|
|
92
|
+
# constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
|
|
93
|
+
env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
|
|
94
|
+
stub_missing_referenced_types(env, resolved, project_files)
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
# rbs ships `stdlib/bigdecimal/` and `stdlib/bigdecimal-math/` as two libraries, so
|
|
@@ -157,32 +160,61 @@ module Rigor
|
|
|
157
160
|
false
|
|
158
161
|
end
|
|
159
162
|
|
|
160
|
-
# Backstop for a
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an
|
|
164
|
-
# this code must not depend on. Resolution
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# `#unload` (rbs 3.x), re-raises into
|
|
163
|
+
# Backstop for a `RBS::DuplicatedDeclarationError` that only materialises at `resolve_type_names`
|
|
164
|
+
# (which rebuilds the env from `sources`). {.add_virtual_rbs} / {.add_project_parsed_decls}
|
|
165
|
+
# transactionally handle the add-time case — empirically everything on rbs 4.x — but the rbs
|
|
166
|
+
# gemspec range spans `>= 3.0, < 5.0` (ADR-79) and WHERE duplicate detection fires is an
|
|
167
|
+
# rbs-internal choice this code must not depend on. Resolution preference: drop colliding VIRTUAL
|
|
168
|
+
# buffers first (explicit `.rbs` wins), else drop colliding PROJECT `signature_paths:` buffers
|
|
169
|
+
# (issue #777 — bundled RBS wins over a kind-colliding project file), then retry. Every pass
|
|
170
|
+
# removes at least one buffer, so the loop is bounded. A duplicate with neither virtual nor
|
|
171
|
+
# project culprits (bundled-vs-bundled), or an env without `#unload` (rbs 3.x), re-raises into
|
|
172
|
+
# the existing one-warning total-failure path.
|
|
169
173
|
#
|
|
170
174
|
# The dropped set is not returned: consumers recover it from the built env via
|
|
171
175
|
# {#virtual_rbs_collision_quarantined}, which also works on a cache HIT where this build never ran.
|
|
172
|
-
def resolve_quarantining_virtual_collisions(env, virtual_rbs)
|
|
176
|
+
def resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: [])
|
|
173
177
|
virtual_names = virtual_rbs.to_set { |name, _content| name.to_s }
|
|
174
|
-
|
|
178
|
+
project_names = Array(project_files).to_set(&:to_s)
|
|
179
|
+
# Bound by virtual + project culprit candidates: each pass removes at least one buffer.
|
|
180
|
+
(virtual_names.size + project_names.size + 1).times do
|
|
175
181
|
return [env, env.resolve_type_names]
|
|
176
182
|
rescue ::RBS::DuplicatedDeclarationError => e
|
|
177
|
-
|
|
183
|
+
env = unload_duplicated_declaration_culprits(
|
|
184
|
+
env, e, virtual_names: virtual_names, project_names: project_names
|
|
185
|
+
)
|
|
186
|
+
end
|
|
187
|
+
[env, env.resolve_type_names]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Prefer dropping colliding VIRTUAL buffers (explicit `.rbs` wins), else PROJECT `signature_paths:`
|
|
191
|
+
# buffers (issue #777 — bundled RBS wins over a kind-colliding project file). A duplicate with
|
|
192
|
+
# neither virtual nor project culprits (bundled-vs-bundled), or an env without `#unload`, re-raises.
|
|
193
|
+
def unload_duplicated_declaration_culprits(env, error, virtual_names:, project_names:)
|
|
194
|
+
raise unless env.respond_to?(:unload)
|
|
178
195
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
culprits = duplicated_declaration_buffer_names(error)
|
|
197
|
+
virtual_culprits = culprits.select { |name| virtual_names.include?(name.to_s) }
|
|
198
|
+
return env.unload(virtual_culprits) unless virtual_culprits.empty?
|
|
199
|
+
|
|
200
|
+
# Prefer dropping the project buffer(s): the bundled declaration stays, Greeter/core stay
|
|
201
|
+
# usable, and only the conflicting file is absent.
|
|
202
|
+
project_culprits = select_project_collision_culprits(culprits, project_names)
|
|
203
|
+
raise if project_culprits.empty?
|
|
204
|
+
|
|
205
|
+
env.unload(project_culprits)
|
|
206
|
+
end
|
|
182
207
|
|
|
183
|
-
|
|
208
|
+
def duplicated_declaration_buffer_names(error)
|
|
209
|
+
error.decls.filter_map { |decl| decl.location&.buffer&.name }.uniq
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def select_project_collision_culprits(culprits, project_names)
|
|
213
|
+
culprits.select do |name|
|
|
214
|
+
project_names.include?(File.expand_path(name.to_s))
|
|
215
|
+
rescue ArgumentError, TypeError
|
|
216
|
+
project_names.include?(name.to_s)
|
|
184
217
|
end
|
|
185
|
-
[env, env.resolve_type_names]
|
|
186
218
|
end
|
|
187
219
|
|
|
188
220
|
# ADR-5 robustness, second tier. A project `signature_paths:` RBS that *references* a type no loaded
|
|
@@ -298,11 +330,26 @@ module Rigor
|
|
|
298
330
|
next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
|
|
299
331
|
|
|
300
332
|
buffer, directives, decls = parsed
|
|
301
|
-
|
|
333
|
+
add_project_parsed_decls(env, buffer, directives, decls)
|
|
302
334
|
end
|
|
303
335
|
add_deferred_signatures(env, deferred)
|
|
304
336
|
end
|
|
305
337
|
|
|
338
|
+
# Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
|
|
339
|
+
# `sources` BEFORE inserting decls, so a mid-insert `RBS::DuplicatedDeclarationError` (class-vs-module
|
|
340
|
+
# kind collision against bundled RBS, or a constant redeclared from core) would otherwise leave a
|
|
341
|
+
# poisoned source that `resolve_type_names` re-raises outside every per-file rescue, collapsing the
|
|
342
|
+
# WHOLE env to nil. Mirror {.add_virtual_rbs}: drop the poisoned source and continue so Greeter /
|
|
343
|
+
# core / every non-conflicting project file still load. Without a `sources` table (rbs 3.x) re-raise
|
|
344
|
+
# into the existing total-failure path — we cannot make the skip transactional there.
|
|
345
|
+
def add_project_parsed_decls(env, buffer, directives, decls)
|
|
346
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
347
|
+
rescue ::RBS::DuplicatedDeclarationError
|
|
348
|
+
raise unless env.respond_to?(:sources)
|
|
349
|
+
|
|
350
|
+
env.sources.reject! { |source| source.buffer.name == buffer.name }
|
|
351
|
+
end
|
|
352
|
+
|
|
306
353
|
# Issue #610 — the DEFERRED half: signature files a bundled plugin contributes through its manifest,
|
|
307
354
|
# added after every other source so each can be asked a question the eager half cannot be asked —
|
|
308
355
|
# "does something already declare this class, at a different generic arity?".
|
|
@@ -331,7 +378,7 @@ module Rigor
|
|
|
331
378
|
buffer, directives, decls = parsed
|
|
332
379
|
next if generic_arity_conflict(env, decls)
|
|
333
380
|
|
|
334
|
-
|
|
381
|
+
add_project_parsed_decls(env, buffer, directives, decls)
|
|
335
382
|
end
|
|
336
383
|
end
|
|
337
384
|
|
|
@@ -436,6 +483,55 @@ module Rigor
|
|
|
436
483
|
end
|
|
437
484
|
end
|
|
438
485
|
|
|
486
|
+
# Issue #777 — project `signature_paths:` files that PARSE but are absent from the built env because
|
|
487
|
+
# {.add_project_parsed_decls} / {.resolve_quarantining_virtual_collisions} quarantined a duplicated
|
|
488
|
+
# declaration (typically class-vs-module or a constant already shipped by bundled RBS). Derived from
|
|
489
|
+
# the env like {#virtual_rbs_collision_quarantined}, so a cache HIT reports the same condition.
|
|
490
|
+
#
|
|
491
|
+
# Path membership alone is not enough: the suite's {RbsEnvMemo} reuses a byte-identical env across
|
|
492
|
+
# `Dir.mktmpdir` roots when `signature_paths:` are relative (`sig/sink.rbs`), leaving buffers named
|
|
493
|
+
# after the FIRST path. A later example's absolute path is then absent from `present` even though
|
|
494
|
+
# the declarations loaded — that used to spam `duplicated declaration against bundled RBS` banners
|
|
495
|
+
# (~60 per CI shard) for fixtures that never collided. Basename + content equality catches that
|
|
496
|
+
# reuse without hiding a real quarantine (e.g. project `class Base64` vs bundled `module Base64`,
|
|
497
|
+
# same basename, different bytes).
|
|
498
|
+
COLLISION_QUARANTINE_NOTE =
|
|
499
|
+
"duplicated declaration against bundled RBS — quarantined so the rest of the RBS env still loads"
|
|
500
|
+
|
|
501
|
+
def collision_quarantined_project_signatures(env, signature_paths)
|
|
502
|
+
return [] if env.nil?
|
|
503
|
+
|
|
504
|
+
present = env.buffers.to_set do |buffer|
|
|
505
|
+
name = buffer.name
|
|
506
|
+
next name.to_s unless name
|
|
507
|
+
|
|
508
|
+
File.expand_path(name.to_s)
|
|
509
|
+
rescue ArgumentError, TypeError
|
|
510
|
+
name.to_s
|
|
511
|
+
end
|
|
512
|
+
buffers_by_basename = env.buffers.group_by { |buffer| File.basename(buffer.name.to_s) }
|
|
513
|
+
project_sig_files(signature_paths).sort.filter_map do |file|
|
|
514
|
+
next if present.include?(file)
|
|
515
|
+
next if parse_signature_file(file).nil? # parse / encoding quarantine owns these
|
|
516
|
+
next if project_signature_loaded_under_other_path?(file, buffers_by_basename)
|
|
517
|
+
|
|
518
|
+
[file, "#{file}: #{COLLISION_QUARANTINE_NOTE}"]
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# True when `file`'s bytes are already in `env` under a different absolute path (same basename).
|
|
523
|
+
# See {#collision_quarantined_project_signatures} — keeps memoised env reuse from looking like a
|
|
524
|
+
# collision quarantine.
|
|
525
|
+
def project_signature_loaded_under_other_path?(file, buffers_by_basename)
|
|
526
|
+
peers = buffers_by_basename[File.basename(file)]
|
|
527
|
+
return false if peers.nil? || peers.empty?
|
|
528
|
+
|
|
529
|
+
content = File.read(file, encoding: "UTF-8")
|
|
530
|
+
peers.any? { |buffer| buffer.content == content }
|
|
531
|
+
rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES
|
|
532
|
+
false
|
|
533
|
+
end
|
|
534
|
+
|
|
439
535
|
# The `::`-stripped names of every type a PROJECT signature references that no loaded declaration
|
|
440
536
|
# provides — the input to {.append_stub_declarations}.
|
|
441
537
|
#
|
|
@@ -1000,8 +1096,10 @@ module Rigor
|
|
|
1000
1096
|
# `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
|
|
1001
1097
|
# names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
|
|
1002
1098
|
# `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
|
|
1003
|
-
# `:
|
|
1004
|
-
# `:
|
|
1099
|
+
# `:definition_build_deferred_count`, `:definition_build_deferred_first`,
|
|
1100
|
+
# `:definition_build_summary_warned`, `:internal_demand`, `:internal_demand_status`, `:builder`,
|
|
1101
|
+
# `:reflection`,
|
|
1102
|
+
# `:instance_definitions_table`, `:singleton_definitions_table`.
|
|
1005
1103
|
# Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
|
|
1006
1104
|
# infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
|
|
1007
1105
|
# `@state[:env_loaded]`) to a constant `nil`.
|
|
@@ -1031,7 +1129,17 @@ module Rigor
|
|
|
1031
1129
|
#
|
|
1032
1130
|
# @return [Array<Array(String, String)>] empty when every `signature_paths:` file parses.
|
|
1033
1131
|
def quarantined_signatures
|
|
1034
|
-
@state[:quarantined] ||=
|
|
1132
|
+
@state[:quarantined] ||= begin
|
|
1133
|
+
parse_quarantined = self.class.quarantined_project_signatures(@signature_paths)
|
|
1134
|
+
collision_quarantined = self.class.collision_quarantined_project_signatures(
|
|
1135
|
+
@state[:env], @signature_paths
|
|
1136
|
+
)
|
|
1137
|
+
parse_paths = parse_quarantined.to_set { |path, _note| path }
|
|
1138
|
+
(
|
|
1139
|
+
parse_quarantined +
|
|
1140
|
+
collision_quarantined.reject { |entry| parse_paths.include?(entry[0]) }
|
|
1141
|
+
).freeze
|
|
1142
|
+
end
|
|
1035
1143
|
end
|
|
1036
1144
|
|
|
1037
1145
|
# Issue #610 — plugin-contributed signature files that stood down against a colliding generic arity,
|
|
@@ -1083,12 +1191,13 @@ module Rigor
|
|
|
1083
1191
|
end
|
|
1084
1192
|
|
|
1085
1193
|
# The total RBS-environment build failure captured this run, or nil when the env built. Unlike
|
|
1086
|
-
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived
|
|
1087
|
-
#
|
|
1088
|
-
#
|
|
1089
|
-
#
|
|
1090
|
-
#
|
|
1091
|
-
# re-
|
|
1194
|
+
# {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived so a
|
|
1195
|
+
# cache HIT reports it too — a total failure collapses the WHOLE env to nil. Project-vs-bundled
|
|
1196
|
+
# `RBS::DuplicatedDeclarationError`s are quarantined per-file since #777; this slot remains for
|
|
1197
|
+
# unrecoverable build errors (e.g. bundled-vs-bundled collisions, or hosts without a transactional
|
|
1198
|
+
# `sources`/`unload` API). A failed build produces no cached success to hide behind (nothing is
|
|
1199
|
+
# persisted, so every run re-attempts and re-raises), so this is captured directly in {#env}'s rescue
|
|
1200
|
+
# rather than re-derived. Forcing `env` (any query does) populates it.
|
|
1092
1201
|
#
|
|
1093
1202
|
# @return [Array(String, String, Array<String>), nil] `[error_class_name, first_error_line,
|
|
1094
1203
|
# conflicting_buffer_names]`, or nil when the environment built successfully.
|
|
@@ -1736,11 +1845,10 @@ module Rigor
|
|
|
1736
1845
|
end
|
|
1737
1846
|
|
|
1738
1847
|
# The RBS environment for this loader. Memoised both on success AND on failure: when the env build
|
|
1739
|
-
# raises
|
|
1740
|
-
#
|
|
1741
|
-
#
|
|
1742
|
-
#
|
|
1743
|
-
# `warn_about_env_build_failure_once` so the broken `signature_paths:` entry is identifiable.
|
|
1848
|
+
# raises an *unrecoverable* `RBS::BaseError` (project-vs-bundled duplicates are quarantined per-file
|
|
1849
|
+
# since #777), retrying on every subsequent `env` call would re-parse and re-resolve the whole sig set
|
|
1850
|
+
# per AST node touched during analysis, multiplying per-file analysis cost by ~100x. Failures
|
|
1851
|
+
# short-circuit to `nil` here and are surfaced to the user via `warn_about_env_build_failure_once`.
|
|
1744
1852
|
def env
|
|
1745
1853
|
return @state[:env] if @state[:env_loaded]
|
|
1746
1854
|
|
|
@@ -1778,10 +1886,11 @@ module Rigor
|
|
|
1778
1886
|
lines = listed.map { |_path, first_line| " - #{first_line}" }
|
|
1779
1887
|
lines << " … and #{more} more" if more.positive?
|
|
1780
1888
|
warn(
|
|
1781
|
-
"rigor: skipped #{quarantined.size}
|
|
1782
|
-
"They were QUARANTINED so the rest of your RBS env
|
|
1783
|
-
"declare are absent — calls into them read
|
|
1784
|
-
"are reduced. Fix the parse error(s)
|
|
1889
|
+
"rigor: skipped #{quarantined.size} RBS file(s) under `signature_paths:` (unparseable or\n " \
|
|
1890
|
+
"duplicated against bundled RBS). They were QUARANTINED so the rest of your RBS env\n " \
|
|
1891
|
+
"still loads, but the types they declare are absent — calls into them read\n " \
|
|
1892
|
+
"`Dynamic[top]`, so coverage and diagnostics are reduced. Fix the parse error(s) or\n " \
|
|
1893
|
+
"remove the conflicting declaration(s) to restore that coverage:\n" \
|
|
1785
1894
|
"#{lines.join("\n")}"
|
|
1786
1895
|
)
|
|
1787
1896
|
end
|
|
@@ -1870,8 +1979,8 @@ module Rigor
|
|
|
1870
1979
|
#
|
|
1871
1980
|
# {#during_internal_demand} (issue #696) — these two walk EVERY known class, so a failure they hit is a
|
|
1872
1981
|
# failure of the sig set, not of anything the run asked about, and it must not reach
|
|
1873
|
-
# {#definition_build_failures}. The stderr
|
|
1874
|
-
#
|
|
1982
|
+
# {#definition_build_failures}. The stderr fallback stays armed, but {#during_internal_demand} folds
|
|
1983
|
+
# every failure found by one outermost walk into one bounded summary (issue #718).
|
|
1875
1984
|
def instance_definitions_table
|
|
1876
1985
|
@state[:instance_definitions_table] ||= during_internal_demand do
|
|
1877
1986
|
build_definitions_table { |name| build_instance_definition(name) }
|
|
@@ -2005,7 +2114,9 @@ module Rigor
|
|
|
2005
2114
|
# review, second pass).
|
|
2006
2115
|
#
|
|
2007
2116
|
# Save-and-restore rather than a bare flag: `#prewarm` wraps a body whose members wrap themselves, and
|
|
2008
|
-
# a nested demand must not un-mark its caller on the way out.
|
|
2117
|
+
# a nested demand must not un-mark its caller on the way out. A successful outermost exit flushes the
|
|
2118
|
+
# deferred stderr summary; an outermost exit that raises keeps the detailed fallback banner armed even
|
|
2119
|
+
# when an earlier internal-demand episode already emitted the loader-instance summary.
|
|
2009
2120
|
#
|
|
2010
2121
|
# Per LOADER, not per thread. Nesting and a raise mid-demand are both handled, and the fork pool forks
|
|
2011
2122
|
# after `#prewarm` returns, so no CLI path shares a loader across concurrent analyses. An in-process
|
|
@@ -2013,10 +2124,29 @@ module Rigor
|
|
|
2013
2124
|
# silence another's reporting; not reachable today, and not worth a thread-local until it is.
|
|
2014
2125
|
def during_internal_demand
|
|
2015
2126
|
previous = @state[:internal_demand]
|
|
2127
|
+
outermost = !previous
|
|
2128
|
+
if outermost
|
|
2129
|
+
@state[:definition_build_deferred_count] = 0
|
|
2130
|
+
@state[:definition_build_deferred_first] = nil
|
|
2131
|
+
end
|
|
2016
2132
|
@state[:internal_demand] = true
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
@state[:
|
|
2133
|
+
# `rescue`/`else` keeps the normal block result precise for the analyzer; the pending marker covers
|
|
2134
|
+
# non-local exits (e.g. `throw`) that unwind through `ensure` without entering either branch.
|
|
2135
|
+
@state[:internal_demand_status] = :pending if outermost
|
|
2136
|
+
begin
|
|
2137
|
+
result = yield
|
|
2138
|
+
@state[:internal_demand_status] = :completed if outermost
|
|
2139
|
+
rescue StandardError, ScriptError => e
|
|
2140
|
+
@state[:internal_demand_status] = :aborted if outermost
|
|
2141
|
+
warn_about_aborted_definition_build_failures if outermost
|
|
2142
|
+
raise e
|
|
2143
|
+
else
|
|
2144
|
+
warn_about_deferred_definition_build_failures if outermost
|
|
2145
|
+
result
|
|
2146
|
+
ensure
|
|
2147
|
+
@state[:internal_demand] = previous
|
|
2148
|
+
warn_about_aborted_definition_build_failures if outermost && @state[:internal_demand_status] == :pending
|
|
2149
|
+
end
|
|
2020
2150
|
end
|
|
2021
2151
|
|
|
2022
2152
|
# The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
|
|
@@ -2029,21 +2159,33 @@ module Rigor
|
|
|
2029
2159
|
# {#env}, because the whole env is built eagerly and every quarantine/collision is already known by
|
|
2030
2160
|
# then. Definition builds are LAZY (ADR-54 WD1 — built on demand per class the FIRST time a caller asks,
|
|
2031
2161
|
# long after {#env} has already run), so there is no later central checkpoint to fire from before the
|
|
2032
|
-
# affected classes even exist.
|
|
2033
|
-
#
|
|
2034
|
-
#
|
|
2035
|
-
#
|
|
2036
|
-
#
|
|
2037
|
-
#
|
|
2038
|
-
# `@state
|
|
2039
|
-
#
|
|
2040
|
-
#
|
|
2162
|
+
# affected classes even exist. An ordinary demand therefore warns inline at the rescue site; Rigor's
|
|
2163
|
+
# own internal demand defers until its outermost boundary and collapses the whole walk to one summary
|
|
2164
|
+
# (issue #718). Both routes share `@state[:definition_build_warned]` (keyed by normalized class name),
|
|
2165
|
+
# so the instance and singleton sides — and any re-entry once the per-loader-instance definition caches memoize
|
|
2166
|
+
# the failure — count or warn at most once per class name, cache-hit runs included.
|
|
2167
|
+
#
|
|
2168
|
+
# `@state` is per LOADER INSTANCE, not process-global. A fork-pool prewarm finishes before the fork, so
|
|
2169
|
+
# that loader instance emits its summary once in the parent and its dedupe state is inherited by the
|
|
2170
|
+
# workers. A separate loader instance (for example, a parameter-inference pre-pass) has its own summary
|
|
2171
|
+
# budget. A failure first reached by an ordinary demand can still warn once in each worker that reaches
|
|
2172
|
+
# it; deduplicating that across processes is out of scope here — see [#295](https://github.com/rigortype/rigor/issues/295).
|
|
2041
2173
|
def warn_about_definition_build_failure(class_name, error)
|
|
2042
2174
|
warned = (@state[:definition_build_warned] ||= {})
|
|
2043
|
-
key = class_name.to_s
|
|
2175
|
+
key = class_name.to_s.delete_prefix("::")
|
|
2044
2176
|
return if warned[key]
|
|
2045
2177
|
|
|
2046
2178
|
warned[key] = true
|
|
2179
|
+
if @state[:internal_demand]
|
|
2180
|
+
@state[:definition_build_deferred_count] = @state.fetch(:definition_build_deferred_count, 0) + 1
|
|
2181
|
+
@state[:definition_build_deferred_first] ||= [class_name.to_s, error]
|
|
2182
|
+
return
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
warn(definition_build_failure_warning(class_name, error))
|
|
2186
|
+
end
|
|
2187
|
+
|
|
2188
|
+
def definition_build_failure_warning(class_name, error)
|
|
2047
2189
|
first_line = error.message.to_s.lines.first.to_s.strip
|
|
2048
2190
|
buffers = definition_build_conflict_buffers(error)
|
|
2049
2191
|
collisions =
|
|
@@ -2056,13 +2198,42 @@ module Rigor
|
|
|
2056
2198
|
lines << " … and #{more} more" if more.positive?
|
|
2057
2199
|
"\n Colliding declaration(s):\n#{lines.join("\n")}"
|
|
2058
2200
|
end
|
|
2059
|
-
|
|
2060
|
-
"rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
|
|
2201
|
+
"rigor: RBS definition build failed for `#{class_name}`: #{error.class}: #{first_line}\n " \
|
|
2061
2202
|
"Rigor still treats the class as known, so calls into it now silently degrade to\n " \
|
|
2062
2203
|
"`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
|
|
2204
|
+
end
|
|
2205
|
+
|
|
2206
|
+
def warn_about_deferred_definition_build_failures
|
|
2207
|
+
return if @state[:definition_build_summary_warned]
|
|
2208
|
+
|
|
2209
|
+
count = @state[:definition_build_deferred_count]
|
|
2210
|
+
return if count.nil? || count.zero?
|
|
2211
|
+
|
|
2212
|
+
@state[:definition_build_summary_warned] = true
|
|
2213
|
+
first_class, first_error = @state[:definition_build_deferred_first]
|
|
2214
|
+
affected = if count == 1
|
|
2215
|
+
"1 class affected"
|
|
2216
|
+
else
|
|
2217
|
+
more = count - 1
|
|
2218
|
+
noun = more == 1 ? "class" : "classes"
|
|
2219
|
+
"… and #{more} more #{noun} affected (#{count} total)"
|
|
2220
|
+
end
|
|
2221
|
+
warn(
|
|
2222
|
+
"#{definition_build_failure_warning(first_class, first_error)}\n " \
|
|
2223
|
+
"Internal whole-universe demand found #{affected}; the " \
|
|
2224
|
+
"`rbs.coverage.definition-build-failed` diagnostic reports the classes the analysis demanded and " \
|
|
2225
|
+
"the culprit member and signature files."
|
|
2063
2226
|
)
|
|
2064
2227
|
end
|
|
2065
2228
|
|
|
2229
|
+
def warn_about_aborted_definition_build_failures
|
|
2230
|
+
count = @state[:definition_build_deferred_count]
|
|
2231
|
+
return if count.nil? || count.zero?
|
|
2232
|
+
|
|
2233
|
+
first_class, first_error = @state[:definition_build_deferred_first]
|
|
2234
|
+
warn(definition_build_failure_warning(first_class, first_error))
|
|
2235
|
+
end
|
|
2236
|
+
|
|
2066
2237
|
# The definition-build twin of {#env_build_conflict_buffers}: the declaration source file(s) named by a
|
|
2067
2238
|
# `RBS::DefinitionBuilder` failure, so {#warn_about_definition_build_failure} can name the colliding
|
|
2068
2239
|
# declarations rather than only the exception's class and message. The payload shape varies by error
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "environment/rbs_loader"
|
|
|
8
8
|
require_relative "environment/reflection"
|
|
9
9
|
require_relative "environment/reporters"
|
|
10
10
|
require_relative "environment/hkt_registry_holder"
|
|
11
|
+
require_relative "environment/failure_slot"
|
|
11
12
|
require_relative "environment/constant_type_cache_holder"
|
|
12
13
|
require_relative "environment/missing_gem_constant_index"
|
|
13
14
|
require_relative "environment/bundle_sig_discovery"
|
|
@@ -88,6 +89,10 @@ module Rigor
|
|
|
88
89
|
# --no-stats` from doing the RBS env build at all.
|
|
89
90
|
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
90
91
|
@hkt_registry_holder = HktRegistryHolder.new
|
|
92
|
+
# Issue #784 — where either stage of the HKT-registry build (the plugin overlay, #791; the RBS
|
|
93
|
+
# `type`-alias scan) records a raise, so it surfaces once for the run instead of once per file — or,
|
|
94
|
+
# for the overlay, instead of aborting the run outright (see {#hkt_registry}).
|
|
95
|
+
@hkt_scan_failure = FailureSlot.new
|
|
91
96
|
@constant_type_cache = ConstantTypeCacheHolder.new
|
|
92
97
|
# ADR-82 WD9 — `[gem_name, version]` pairs for the locked gems with no resolvable RBS. The
|
|
93
98
|
# root-constant ownership index over them is built lazily (first unresolved constant read) so runs
|
|
@@ -104,21 +109,93 @@ module Rigor
|
|
|
104
109
|
# beat plugin entries, which beat the bundled JSON_VALUE. Memoised; single-threaded use only (under the
|
|
105
110
|
# Ractor pool path each worker has its own Environment so cross-worker mutation is impossible; the LSP
|
|
106
111
|
# single-publish-at-a-time invariant serialises here).
|
|
112
|
+
#
|
|
113
|
+
# BOTH stages are guarded (#784, #791): whichever raises is recorded on {#hkt_scan_failure} with the
|
|
114
|
+
# stage that failed and the other stage still runs, so this getter never raises into its callers —
|
|
115
|
+
# neither into a file's `analyze_body` rescue nor out of the run-owned demands, which have no rescue
|
|
116
|
+
# above them at all.
|
|
107
117
|
def hkt_registry
|
|
108
118
|
@hkt_registry_holder.fetch do
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
pre_scan = pre_scan_hkt_registry
|
|
120
|
+
begin
|
|
121
|
+
Inference::HktRegistry.scan_rbs_loader(
|
|
122
|
+
@rbs_loader,
|
|
123
|
+
base: pre_scan,
|
|
124
|
+
reporter: rbs_extended_reporter
|
|
125
|
+
)
|
|
126
|
+
rescue StandardError => e
|
|
127
|
+
# Issue #784 — the seam. This build is shared and memoised, and it is first demanded from inside
|
|
128
|
+
# a file's analysis, so a raise here would otherwise land in every file's `analyze_body` rescue
|
|
129
|
+
# and turn the whole run into N identical `internal analyzer error` rows (issue #776). Record it
|
|
130
|
+
# and degrade to the pre-scan registry: analysis proceeds, HKT inference from RBS `type` aliases
|
|
131
|
+
# reads its bound (`Dynamic[top]`, ADR-20 WD2), and the run surfaces ONE
|
|
132
|
+
# `rbs.coverage.hkt-scan-failed` row. Not a silent skip — the row is `:error` and names the
|
|
133
|
+
# exception and raise site — and the same shape as `RbsLoader#record_env_build_failure`.
|
|
134
|
+
# Memoising the degraded registry is what keeps the scan from being re-attempted per file.
|
|
135
|
+
record_hkt_registry_failure(e, stage: :scan)
|
|
136
|
+
pre_scan
|
|
137
|
+
end
|
|
119
138
|
end
|
|
120
139
|
end
|
|
121
140
|
|
|
141
|
+
# Issue #791 — the OTHER build behind {#hkt_registry}, and the reason the seam covers two stages
|
|
142
|
+
# rather than one. Aggregating the loaded plugins' manifest-declared HKT entries runs plugin-authored
|
|
143
|
+
# code (`Plugin::Registry#hkt_overlay_registry` reads each manifest); before this it sat one line ABOVE
|
|
144
|
+
# the #784 rescue, so a raise there escaped {#hkt_registry} entirely. Post-#788 that is not a per-file
|
|
145
|
+
# crash storm but an uncaught abort: {HktRegistryHolder#fetch} memoises only on success, so the raise
|
|
146
|
+
# is re-attempted at every demand, and the run-owned demands (`Runner::PoolCoordinator#hkt_scan_outcome`
|
|
147
|
+
# after the file loop, `Analysis::WorkerSession#drain_reporters`) sit outside any rescue.
|
|
148
|
+
#
|
|
149
|
+
# The degradation is narrower than the scan's: only the plugin overlay is dropped, and the RBS scan
|
|
150
|
+
# still runs on top of the bundled base, so a user's own `.rbs` HKT registrations survive a plugin
|
|
151
|
+
# defect. First-write-wins on the slot means a run whose overlay AND scan both raise reports the
|
|
152
|
+
# overlay — the actionable one, since the plugin is what a user can remove.
|
|
153
|
+
#
|
|
154
|
+
# @return [Rigor::Inference::HktRegistry] the registry the RBS scan is layered on top of.
|
|
155
|
+
def pre_scan_hkt_registry
|
|
156
|
+
return @hkt_registry_base unless @plugin_registry.respond_to?(:hkt_overlay_registry)
|
|
157
|
+
|
|
158
|
+
@hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
record_hkt_registry_failure(e, stage: :overlay)
|
|
161
|
+
@hkt_registry_base
|
|
162
|
+
end
|
|
163
|
+
private :pre_scan_hkt_registry
|
|
164
|
+
|
|
165
|
+
# Issue #784 — the `[error_class_name, first_message_line, raw_frame_or_nil, stage]` tuple the seam in
|
|
166
|
+
# {#hkt_registry} recorded, or nil when both stages built (or were never demanded). `stage` is `:scan`
|
|
167
|
+
# for the RBS `type`-alias scan and `:overlay` for the plugin-manifest aggregation (#791); it decides
|
|
168
|
+
# the row's wording, because "the implicit HKT scan over RBS `type` aliases raised" would point a user
|
|
169
|
+
# at their own `.rbs` for a plugin's defect. The run reads the tuple only after demanding
|
|
170
|
+
# {#hkt_registry} itself — after the file loop on the sequential paths, at drain time in each pool
|
|
171
|
+
# worker (the coordinator never analyses a file under the pool, so the drain is how its snapshot learns
|
|
172
|
+
# the outcome: the #696 lesson), and from a resolved environment when the analyze set is empty — so the
|
|
173
|
+
# row never depends on which files happened to be analysed.
|
|
174
|
+
#
|
|
175
|
+
# @return [Array(String, String, String, Symbol), Array(String, String, nil, Symbol), nil]
|
|
176
|
+
def hkt_scan_failure
|
|
177
|
+
@hkt_scan_failure.value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def record_hkt_registry_failure(error, stage:)
|
|
181
|
+
frames = error.backtrace || []
|
|
182
|
+
frame = frames.find { |f| f.include?("/lib/rigor/") } || frames.first
|
|
183
|
+
# `Class#name` is nil for an anonymous exception class; its nearest NAMED ancestor is the stable
|
|
184
|
+
# description (`inspect` embeds an object address, which would make the row's text differ across
|
|
185
|
+
# processes). Each String is frozen individually, not just the Array: the tuple crosses the fork
|
|
186
|
+
# boundary Marshal-clean and the drain channel's stated invariant
|
|
187
|
+
# ({Analysis::WorkerSession#drain_reporters}) is that its payload is also `Ractor.shareable?`, which a
|
|
188
|
+
# shallow freeze over `chomp`'s fresh String would not satisfy.
|
|
189
|
+
named = error.class.ancestors.find { |a| a.is_a?(Class) && a.name }
|
|
190
|
+
@hkt_scan_failure.record([
|
|
191
|
+
(error.class.name || "anonymous #{named&.name || 'Exception'}").dup.freeze,
|
|
192
|
+
error.message.to_s.lines.first.to_s.chomp.freeze,
|
|
193
|
+
frame&.dup&.freeze,
|
|
194
|
+
stage
|
|
195
|
+
])
|
|
196
|
+
end
|
|
197
|
+
private :record_hkt_registry_failure
|
|
198
|
+
|
|
122
199
|
# ADR-82 WD9 — the gem name owning `root_constant_name`, when that gem is locked in the project's
|
|
123
200
|
# Gemfile.lock, ships no resolvable RBS, and its entry file declares the constant at top level. Nil for
|
|
124
201
|
# everything else — the caller then keeps the generic provenance cause. The index is built once, lazily,
|
|
@@ -150,7 +150,9 @@ module Rigor
|
|
|
150
150
|
# loaded RBS env, parse them via {Rigor::RbsExtended::HktDirectives}, and return a new
|
|
151
151
|
# registry that is the union of `base` and every parsed entry. Last-write-wins on URI
|
|
152
152
|
# collisions per {#merge}'s contract. Fail-soft on per-annotation parse errors (the reporter
|
|
153
|
-
# records an
|
|
153
|
+
# records an entry the run surfaces as one `dynamic.rbs-extended.hkt-directive-invalid` `:info`
|
|
154
|
+
# diagnostic; the other annotations still apply). That claim was false until issue #785: the
|
|
155
|
+
# production reporter has no `#record`, so every declined directive was dropped silently.
|
|
154
156
|
#
|
|
155
157
|
# @param rbs_loader [Rigor::Environment::RbsLoader]
|
|
156
158
|
# @param base [HktRegistry] starting registry (typically the bundled
|
|
@@ -158,8 +160,9 @@ module Rigor
|
|
|
158
160
|
# @param name_scope [Rigor::Environment::NameScope, nil] threaded through to the bound
|
|
159
161
|
# resolver for class-name lookups; safe to omit during scanning since hkt bounds are
|
|
160
162
|
# typically `untyped` or stdlib classes.
|
|
161
|
-
# @param reporter [#record, nil] same fail-soft reporter contract
|
|
162
|
-
# parsers use
|
|
163
|
+
# @param reporter [Rigor::RbsExtended::Reporter, #record, nil] same fail-soft reporter contract
|
|
164
|
+
# the other RBS-extended parsers use; a collecting double that responds only to `#record` /
|
|
165
|
+
# `#<<` still works (see {Rigor::RbsExtended::HktDirectives.record_hkt_error}).
|
|
163
166
|
|
|
164
167
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
165
168
|
def self.scan_rbs_loader(rbs_loader, base: EMPTY, name_scope: nil, reporter: nil)
|
|
@@ -198,7 +201,16 @@ module Rigor
|
|
|
198
201
|
params = decl.type_params.map(&:name)
|
|
199
202
|
params_set = params.to_set
|
|
200
203
|
|
|
201
|
-
|
|
204
|
+
# No rescue around this translate. HktSugarTranslator degrades every alias shape it
|
|
205
|
+
# cannot model (a non-recursive alias, a malformed self-reference) to a leaf, so a
|
|
206
|
+
# parseable `type` alias never raises here — that is the fix for issue #776, where an
|
|
207
|
+
# unknown keyword aborted this shared, memoised build and turned every file into an
|
|
208
|
+
# `internal analyzer error`. An exception that still reaches this point is an analyzer
|
|
209
|
+
# bug and MUST propagate rather than silently dropping the alias, matching
|
|
210
|
+
# RbsLoader#each_known_class_name's fail-soft contract. It propagates to the seam in
|
|
211
|
+
# `Environment#hkt_registry` (issue #784), which records it and degrades to the pre-scan
|
|
212
|
+
# registry so it surfaces once per run instead of reaching every file's analysis.
|
|
213
|
+
translator = HktSugarTranslator.new(uri: uri, params_set: params_set)
|
|
202
214
|
body_tree = translator.translate(decl.type)
|
|
203
215
|
|
|
204
216
|
next unless translator.recursive
|