rigortype 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/manual/02-cli-reference.md +16 -7
  15. data/docs/manual/05-inspecting-types.md +20 -8
  16. data/docs/manual/07-plugins.md +4 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  18. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  20. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  21. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  22. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  24. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  28. data/lib/rigor/analysis/check_rules.rb +383 -48
  29. data/lib/rigor/analysis/crash_signature.rb +116 -0
  30. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  32. data/lib/rigor/analysis/incremental.rb +28 -0
  33. data/lib/rigor/analysis/incremental_session.rb +44 -9
  34. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  35. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  36. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  37. data/lib/rigor/analysis/result.rb +32 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  43. data/lib/rigor/analysis/runner.rb +147 -10
  44. data/lib/rigor/analysis/worker_session.rb +10 -1
  45. data/lib/rigor/bleeding_edge.rb +7 -6
  46. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  48. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  49. data/lib/rigor/cache/descriptor.rb +42 -2
  50. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  51. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  52. data/lib/rigor/cache/store.rb +10 -1
  53. data/lib/rigor/cli/coverage_command.rb +23 -18
  54. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  55. data/lib/rigor/cli/coverage_scan.rb +47 -6
  56. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  57. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  58. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  59. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  60. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  61. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  62. data/lib/rigor/cli/type_of_command.rb +188 -41
  63. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  64. data/lib/rigor/cli/type_scan_command.rb +10 -1
  65. data/lib/rigor/cli.rb +1 -1
  66. data/lib/rigor/configuration.rb +7 -4
  67. data/lib/rigor/effects/plugin_facts.rb +1 -1
  68. data/lib/rigor/effects/scanner.rb +2 -3
  69. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  70. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  71. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  72. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  73. data/lib/rigor/environment/rbs_loader.rb +525 -46
  74. data/lib/rigor/environment.rb +38 -15
  75. data/lib/rigor/inference/acceptance.rb +172 -20
  76. data/lib/rigor/inference/captured_locals.rb +62 -0
  77. data/lib/rigor/inference/content_join.rb +347 -0
  78. data/lib/rigor/inference/def_handle.rb +15 -3
  79. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  80. data/lib/rigor/inference/expression_typer.rb +1020 -141
  81. data/lib/rigor/inference/fork_map.rb +6 -1
  82. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  83. data/lib/rigor/inference/hkt_registry.rb +46 -0
  84. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  85. data/lib/rigor/inference/index_write_widening.rb +48 -0
  86. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  89. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  93. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  95. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  96. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  97. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  98. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  99. data/lib/rigor/inference/mutation_widening.rb +244 -174
  100. data/lib/rigor/inference/narrowing.rb +331 -27
  101. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  102. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  103. data/lib/rigor/inference/precision_scanner.rb +5 -2
  104. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  105. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  106. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  107. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  108. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  109. data/lib/rigor/inference/version_guard.rb +229 -0
  110. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  111. data/lib/rigor/plugin/base.rb +3 -2
  112. data/lib/rigor/plugin/inflector.rb +14 -5
  113. data/lib/rigor/plugin/io_boundary.rb +107 -4
  114. data/lib/rigor/plugin/loader.rb +14 -0
  115. data/lib/rigor/protection/analysis_guard.rb +60 -0
  116. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  117. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  118. data/lib/rigor/protection/discovery_seed.rb +1 -1
  119. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  120. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  121. data/lib/rigor/protection/mutator.rb +121 -18
  122. data/lib/rigor/reflection.rb +182 -30
  123. data/lib/rigor/scope/discovery_index.rb +39 -1
  124. data/lib/rigor/scope.rb +385 -4
  125. data/lib/rigor/sig_gen/classification.rb +12 -1
  126. data/lib/rigor/sig_gen/generator.rb +254 -5
  127. data/lib/rigor/sig_gen/renderer.rb +1 -6
  128. data/lib/rigor/sig_gen/writer.rb +3 -0
  129. data/lib/rigor/source/constant_path.rb +79 -2
  130. data/lib/rigor/triage/catalogue.rb +1 -1
  131. data/lib/rigor/type/combinator.rb +10 -0
  132. data/lib/rigor/type/maybe.rb +47 -0
  133. data/lib/rigor/type/refined.rb +1 -2
  134. data/lib/rigor/type/result.rb +53 -0
  135. data/lib/rigor/type.rb +2 -0
  136. data/lib/rigor/version.rb +1 -1
  137. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  139. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  140. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  141. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  142. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  147. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  148. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  150. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  151. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  152. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  153. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  154. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  156. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  157. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  159. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  160. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  163. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  164. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  165. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  166. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  167. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  168. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  169. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  170. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  172. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  173. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  174. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  175. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  176. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  177. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  178. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  179. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  180. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  181. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  182. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  183. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  184. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  185. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  186. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  187. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  188. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  189. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  190. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  191. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  192. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  193. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  194. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  195. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  196. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  200. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  201. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  202. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  203. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  204. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  205. data/sig/rigor/analysis/baseline.rbs +1 -1
  206. data/sig/rigor/environment.rbs +3 -0
  207. data/sig/rigor/inference.rbs +4 -1
  208. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  209. data/sig/rigor/reflection.rbs +3 -1
  210. data/sig/rigor/scope.rbs +25 -1
  211. data/sig/rigor/type.rbs +95 -1
  212. data/sig/rigor.rbs +16 -0
  213. metadata +69 -3
@@ -23,7 +23,7 @@ module Rigor
23
23
  # API stays purely functional.
24
24
  #
25
25
  # See docs/internal-spec/inference-engine.md for the binding contract.
26
- # rubocop:disable Metrics/ClassLength
26
+ # rubocop:disable-next Metrics/ClassLength
27
27
  class RbsLoader
28
28
  # Buffer name stamped on the `module` declarations synthesized by {.synthesize_missing_namespaces}.
29
29
  # Re-read off the built env by {#synthesized_namespaces} so the analysis layer can surface an `:info`
@@ -38,6 +38,10 @@ module Rigor
38
38
  # into the set {MethodDispatcher} resolves to `Dynamic[Top]` (no false `call.undefined-method`).
39
39
  SYNTHETIC_STUB_BUFFER = "(rigor: synthesized stub types)"
40
40
 
41
+ # The buffer name `Cache::RbsEnvironmentMarshalPatch` reconstructs every `RBS::Location` behind on a
42
+ # cache HIT. Never a real path, so it must never be reported as one (issue #696).
43
+ CACHED_LOCATION_BUFFER_NAME = "<cached>"
44
+
41
45
  # Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
42
46
  # before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
43
47
  # errors buries the signal.
@@ -63,7 +67,7 @@ module Rigor
63
67
  # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the
64
68
  # per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
65
69
  # intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
66
- def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
70
+ def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [])
67
71
  rbs_loader = RBS::EnvironmentLoader.new
68
72
  libraries = libraries_without_shadowed_bigdecimal_math(libraries)
69
73
  loaded_libraries = libraries.select do |library|
@@ -80,7 +84,7 @@ module Rigor
80
84
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
81
85
  add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s))
82
86
  env = RBS::Environment.from_loader(rbs_loader)
83
- add_project_signatures(env, signature_paths)
87
+ add_project_signatures(env, signature_paths, deferred_signature_paths)
84
88
  add_virtual_rbs(env, virtual_rbs)
85
89
  synthesize_missing_namespaces(env)
86
90
  env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs)
@@ -173,7 +177,7 @@ module Rigor
173
177
  raise unless env.respond_to?(:unload)
174
178
 
175
179
  culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name }
176
- .uniq.select { |name| virtual_names.include?(name) }
180
+ .uniq.select { |name| virtual_names.include?(name) }
177
181
  raise if culprits.empty?
178
182
 
179
183
  env = env.unload(culprits)
@@ -287,14 +291,98 @@ module Rigor
287
291
  # Buffer names are the file's absolute path (matching {.project_sig_files}) so {.project_entry?} — which
288
292
  # attributes a `class_decls` entry to the project by buffer name — still recognises these declarations.
289
293
  # Sorted for a deterministic add order (the env feeds the cache, ADR-54).
290
- def add_project_signatures(env, signature_paths)
291
- project_sig_files(signature_paths).sort.each do |file|
294
+ def add_project_signatures(env, signature_paths, deferred_paths = [])
295
+ deferred = project_sig_files(deferred_paths)
296
+ (project_sig_files(signature_paths) - deferred).sort.each do |file|
292
297
  parsed = parse_signature_file(file)
293
298
  next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
294
299
 
295
300
  buffer, directives, decls = parsed
296
301
  add_parsed_decls(env, buffer, directives, decls)
297
302
  end
303
+ add_deferred_signatures(env, deferred)
304
+ end
305
+
306
+ # Issue #610 — the DEFERRED half: signature files a bundled plugin contributes through its manifest,
307
+ # added after every other source so each can be asked a question the eager half cannot be asked —
308
+ # "does something already declare this class, at a different generic arity?".
309
+ #
310
+ # `plugins/rigor-activerecord/sig` declares `class Relation[Elem]`; `rbs collection install` ships a
311
+ # NON-generic `ActiveRecord::Relation`. Two declarations of one class with different arity make
312
+ # `RBS::DefinitionBuilder` raise `GenericParameterMismatchError`, and Rigor keeps the class KNOWN
313
+ # after a failed build, so every call INTO a relation reads `Dynamic[top]` — the plugin and
314
+ # `rigor-project-init`'s own advice to install the collection cancel each other out.
315
+ #
316
+ # The plugin stands down, not the user's source, for the reason `Reflection.constant_type_at`
317
+ # already encodes: the project's own sources are authoritative for the project. Standing down is
318
+ # strictly better than the collision it replaces — the collection's own declaration builds, where
319
+ # today NEITHER does — and it cannot regress a project without a colliding declaration, because
320
+ # nothing triggers.
321
+ #
322
+ # Granularity is the FILE, not the declaration: a bundled plugin's `sig/` file is single-purpose
323
+ # (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
324
+ # exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
325
+ # innocent ones is the change to make if that ever stops being true.
326
+ def add_deferred_signatures(env, deferred_files)
327
+ deferred_files.sort.each do |file|
328
+ parsed = parse_signature_file(file)
329
+ next if parsed.nil?
330
+
331
+ buffer, directives, decls = parsed
332
+ next if generic_arity_conflict(env, decls)
333
+
334
+ add_parsed_decls(env, buffer, directives, decls)
335
+ end
336
+ end
337
+
338
+ # Issue #610 — which deferred (plugin-contributed) files stood down, as
339
+ # `[absolute_path, class_name, existing_arity, incoming_arity]`, sorted. DERIVED from a built env
340
+ # rather than recorded during the build, for {RbsLoader#quarantined_signatures}' reason: a cache HIT
341
+ # never runs the build, and a condition only the build knew would silently disappear on the second
342
+ # run. Re-asking the same question of the FINAL env gives the same answer — the file's declaration is
343
+ # absent from it, and the source that displaced it is still there with its own arity.
344
+ def deferred_standdowns(env, deferred_paths)
345
+ return [] if env.nil? || deferred_paths.nil? || deferred_paths.empty?
346
+
347
+ project_sig_files(deferred_paths).sort.filter_map do |file|
348
+ parsed = parse_signature_file(file)
349
+ next if parsed.nil?
350
+
351
+ conflict = generic_arity_conflict(env, parsed[2])
352
+ conflict && [file, *conflict]
353
+ end
354
+ end
355
+
356
+ # The first `[class_name, existing_arity, incoming_arity]` where `decls` would re-declare a class the
357
+ # env already has with a DIFFERENT number of type parameters, or nil when they agree everywhere.
358
+ # Equal arity is not a conflict: RBS reopens the class, which is the ordinary and supported case, so
359
+ # the stand-down must not fire on it.
360
+ def generic_arity_conflict(env, decls)
361
+ each_declared_class(decls) do |name, arity|
362
+ entry = env.class_decls[::RBS::TypeName.parse(name)]
363
+ next if entry.nil? || !entry.respond_to?(:type_params)
364
+
365
+ existing = entry.type_params&.size
366
+ return [name, existing, arity] if existing && existing != arity
367
+ end
368
+ nil
369
+ end
370
+
371
+ # Yields `[absolute_class_name, type_param_count]` for every class declared anywhere in `decls`,
372
+ # descending through modules and nested classes. Names inside a `module`/`class` body are parsed
373
+ # RELATIVE, so the enclosing path is accumulated here — the env is keyed by absolute name, and
374
+ # comparing a relative `Relation` against it would silently never match.
375
+ def each_declared_class(decls, prefix = [], &block)
376
+ decls.each do |decl|
377
+ case decl
378
+ when ::RBS::AST::Declarations::Module
379
+ each_declared_class(decl.members, prefix + [decl.name.to_s], &block)
380
+ when ::RBS::AST::Declarations::Class
381
+ inner = prefix + [decl.name.to_s]
382
+ block.call("::#{inner.join('::')}", decl.type_params.size)
383
+ each_declared_class(decl.members, inner, &block)
384
+ end
385
+ end
298
386
  end
299
387
 
300
388
  # The quarantine note for a file rejected by {.invalid_encoding?} — worded to be distinct from any
@@ -758,6 +846,79 @@ module Rigor
758
846
  end
759
847
  end
760
848
 
849
+ # @param path [String, Pathname] typically an entry from an {RbsLoader} instance's
850
+ # {#signature_paths}.
851
+ # @return [Boolean] whether `path` sits under the bundled gem-overlay root — what
852
+ # `CheckRules::GEM_OVERLAY_OPEN_RECEIVERS`'s gate consults so that a class name's membership in
853
+ # that list alone never grants the open-receiver exemption; the overlay directory that made the
854
+ # entry true must have actually loaded THIS run too (issue #632, tracked further by #660).
855
+ # `GEM_OVERLAY_SIGS_ROOT` is defined inside this `class << self` block, so it is reachable from
856
+ # here directly but NOT as `RbsLoader::GEM_OVERLAY_SIGS_ROOT` from outside — this method is the
857
+ # public seam callers outside this class use instead of reaching for the constant themselves.
858
+ def under_gem_overlay_root?(path)
859
+ path.to_s.start_with?("#{GEM_OVERLAY_SIGS_ROOT}/")
860
+ end
861
+
862
+ # The twin of {.under_gem_overlay_root?}, for the OTHER copy of the same declarations. ADR-72 ships
863
+ # each overlay's surface twice — as `data/gem_overlay/<gem>/` and as the opt-in plugin's own `sig/` —
864
+ # and a project can reach the second WITHOUT the plugin registry, by naming it in `signature_paths:`
865
+ # (issue #672). Two callers ask, for different reasons:
866
+ #
867
+ # - `Environment.gem_overlay_paths` asks per gem, so the overlay stands down instead of loading
868
+ # alongside the twin and collapsing every class they share;
869
+ # - `CheckRules#gem_overlay_loaded?` asks across all of them, because the twin's `sig/` carries no
870
+ # plugin manifest, so `open_receivers:` cannot protect the partial `ActiveSupport::Duration`
871
+ # declaration it nonetheless loads.
872
+ #
873
+ # **Both ask what an entry LOADS, not what it looks like.** Comparing path strings is wrong in both
874
+ # directions, and the false-positive direction is the expensive one: an entry naming the twin's
875
+ # `.rbs` FILE, or a subdirectory of the twin that does not exist, matches as a string while the
876
+ # loader reads nothing from it ({.project_sig_files} takes directories only — the same fact
877
+ # {Rigor::SignaturePathAudit} reports as `:not_directory` / `:missing`). Standing an overlay down for
878
+ # those leaves the project with NEITHER copy, so ordinary `3.minutes` draws `call.undefined-method`
879
+ # on correct code. The quiet direction is the mirror: a symlink to the twin, or a case-variant
880
+ # spelling of it where the filesystem folds case, loads the twin while matching no prefix. Asking
881
+ # {.project_sig_files} for the twin's files and canonicalising both sides answers both.
882
+ #
883
+ # Lives here rather than on {Rigor::Environment} because it is an internal seam, not ADR-2 public
884
+ # API: `spec/rigor/public_api_drift_spec.rb` pins `Environment`'s singleton surface, and a predicate
885
+ # two call sites share is not a promise to plugin authors. Here it sits beside the overlay-side
886
+ # answer to the very same question, on the class that already owns the overlay's layout
887
+ # ({GEM_OVERLAY_SIGS_ROOT}, {.gem_overlay_sig_paths}).
888
+ #
889
+ # @param signature_paths [Array<String, Pathname>] typically an {RbsLoader} instance's
890
+ # {#signature_paths}. Takes the whole list rather than one entry so the twin's file set resolves
891
+ # once per question — `CheckRules` asks it per `ActiveSupport::Duration` receiver.
892
+ def gem_overlay_twin_signatures_loaded?(signature_paths)
893
+ # `GEM_OVERLAY_PLUGIN_IDS` is ADR-72 eligibility policy and stays owned by `Environment`; it is
894
+ # read here only to enumerate which bundled plugins HAVE an overlay twin.
895
+ GEM_OVERLAY_PLUGIN_IDS.each_value.any? do |plugin_id|
896
+ bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
897
+ end
898
+ end
899
+
900
+ # The per-plugin half of {.gem_overlay_twin_signatures_loaded?}. `Environment` owns the gem → plugin
901
+ # id mapping and passes the id; this resolves the id to the engine's own bundled `sig/` and answers
902
+ # the filesystem question.
903
+ #
904
+ # @param plugin_id [String] a manifest id, e.g. `"activesupport-core-ext"`.
905
+ def bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
906
+ return false if signature_paths.nil? || signature_paths.empty?
907
+
908
+ # In-method for the reason `Plugin::FirstParty` documents at its own `require`: the plugin
909
+ # subsystem's load graph reaches back into the environment, and only the overlay path calls this —
910
+ # a run with no eligible overlay gem never pays them.
911
+ require_relative "../plugin/first_party"
912
+ require_relative "../plugin/loader"
913
+ twin = Plugin::Loader.bundled_plugin_sig_path("#{Plugin::FirstParty::GEM_PREFIX}#{plugin_id}")
914
+ return false unless twin
915
+
916
+ twin_files = project_sig_files([twin]).filter_map { |file| canonical_real_path(file) }
917
+ return false if twin_files.empty?
918
+
919
+ signature_paths.any? { |entry| signature_entry_loads?(entry, twin_files) }
920
+ end
921
+
761
922
  def vendored_gem_sig_paths
762
923
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
763
924
 
@@ -778,6 +939,34 @@ module Rigor
778
939
  File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child))
779
940
  end
780
941
  end
942
+
943
+ # Everything below is internal to {.bundled_overlay_twin_signatures_loaded?}. `private` (not
944
+ # `private_class_method`) because inside `class << self` these are instance methods of the
945
+ # singleton class; `private_class_method` looks for them one level further out and raises NameError
946
+ # at load. Placed last so the modifier cannot capture a method that is meant to be reachable.
947
+ private
948
+
949
+ # Whether one `signature_paths:` entry is a directory the loader would read one of `twin_files`
950
+ # from. Both sides are canonicalised first, which is what makes a symlinked or case-variant
951
+ # spelling of the twin answer the same as the twin itself.
952
+ def signature_entry_loads?(entry, twin_files)
953
+ dir = canonical_real_path(entry)
954
+ return false unless dir && File.directory?(dir)
955
+
956
+ prefix = "#{dir}#{File::SEPARATOR}"
957
+ twin_files.any? { |file| file.start_with?(prefix) }
958
+ end
959
+
960
+ # `File.realpath` rather than `File.expand_path`: it resolves symlinks and, on a case-insensitive
961
+ # filesystem, the on-disk spelling — the two ways a path can reach the twin's files without looking
962
+ # like it. It raises for a path that does not exist, which is the answer we want (an entry that
963
+ # resolves to nothing loads nothing), so the rescue returns nil rather than falling back to a
964
+ # lexical expansion that would resurrect the string match.
965
+ def canonical_real_path(path)
966
+ File.realpath(path.to_s)
967
+ rescue SystemCallError
968
+ nil
969
+ end
781
970
  end
782
971
 
783
972
  attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
@@ -797,15 +986,22 @@ module Rigor
797
986
  # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
798
987
  # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
799
988
  # synthesizer-emitting plugin is loaded.
800
- def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [])
989
+ def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
990
+ deferred_signature_paths: [])
801
991
  @libraries = libraries.map(&:to_s).freeze
802
992
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
993
+ # Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
994
+ # allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
995
+ # is also in `@signature_paths`, so the env cache descriptor already digests these files.
996
+ @deferred_signature_paths = deferred_signature_paths.map { |p| Pathname(p) }.freeze
803
997
  @cache_store = cache_store
804
998
  @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
805
999
  # Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be
806
1000
  # `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot
807
1001
  # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:definition_build_warned`,
808
- # `:builder`, `:reflection`, `:instance_definitions_table`, `:singleton_definitions_table`.
1002
+ # `:definition_build_details`, `:definition_build_reported`, `:definition_build_failures`,
1003
+ # `:internal_demand`, `:builder`, `:reflection`, `:instance_definitions_table`,
1004
+ # `:singleton_definitions_table`.
809
1005
  # Constructed via `Hash.new` (NOT a `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't
810
1006
  # infer a fixed key set from the initial state and fold post-initial slot reads (e.g.
811
1007
  # `@state[:env_loaded]`) to a constant `nil`.
@@ -838,6 +1034,54 @@ module Rigor
838
1034
  @state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze
839
1035
  end
840
1036
 
1037
+ # Issue #610 — plugin-contributed signature files that stood down against a colliding generic arity,
1038
+ # as `[absolute_path, class_name, existing_arity, incoming_arity]`. Empty whenever no plugin sig
1039
+ # collides, which is every project that has not run `rbs collection install` for a gem a bundled
1040
+ # plugin also declares.
1041
+ def signature_standdowns
1042
+ @state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
1043
+ end
1044
+
1045
+ # Issue #735 — the class / module names whose PRIMARY declaration lives under the project's own
1046
+ # `signature_paths:`, top-level prefix stripped ("::Admin::Widget" reads back as "Admin::Widget").
1047
+ #
1048
+ # The distinction it exists to draw: a BUNDLED declaration (core, stdlib, a gem's RBS) is
1049
+ # authoritative about its class, so a project `def` on that class is a monkey-patch and ADR-17 has
1050
+ # the analyzer report it rather than adopt it. A declaration the project wrote itself — by hand or
1051
+ # through `rigor sig-gen --write` — is a sidecar of the same source tree, and a `def` in another of
1052
+ # the project's own files is not a monkey-patch, just the second file of an ordinary class. Reading
1053
+ # a partial sidecar as authoritative made `sig-gen --write` on redmine report 5.9x more
1054
+ # `call.undefined-method` than the same project with no `sig/` at all.
1055
+ #
1056
+ # Memoised per loader: one pass over `class_decls` (~1-2k entries), and every consumer is a check
1057
+ # rule on the analysis hot path. Attribution is by buffer NAME, which survives the ADR-54 environment
1058
+ # cache since #725 — an environment blob written before that carries the `<cached>` sentinel instead,
1059
+ # which lands every class outside this set and leaves the pre-#735 behaviour, never a new silence.
1060
+ def project_declared_classes
1061
+ @state[:project_declared_classes] ||= build_project_declared_classes
1062
+ end
1063
+
1064
+ def build_project_declared_classes
1065
+ environment = env
1066
+ return Set.new.freeze if environment.nil?
1067
+
1068
+ project_files = self.class.project_sig_files(@signature_paths)
1069
+ return Set.new.freeze if project_files.empty?
1070
+
1071
+ names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1072
+ acc << rbs_name.to_s.delete_prefix("::") if self.class.project_entry?(entry, project_files)
1073
+ end
1074
+ names.freeze
1075
+ rescue ::RBS::BaseError
1076
+ Set.new.freeze
1077
+ end
1078
+ private :build_project_declared_classes
1079
+
1080
+ # True when `class_name`'s RBS declaration is one the project wrote (see {#project_declared_classes}).
1081
+ def project_declared_class?(class_name)
1082
+ project_declared_classes.include?(class_name.to_s.delete_prefix("::"))
1083
+ end
1084
+
841
1085
  # The total RBS-environment build failure captured this run, or nil when the env built. Unlike
842
1086
  # {#quarantined_signatures} — which the env survives, one file lighter, and which is re-derived by
843
1087
  # re-parsing so a cache HIT reports it too — a total failure (typically `RBS::DuplicatedDeclarationError`:
@@ -853,6 +1097,30 @@ module Rigor
853
1097
  @state[:env_build_failure]
854
1098
  end
855
1099
 
1100
+ # Issue #696 — the PER-CLASS sibling of {#env_build_failure}, one tier quieter in consequence: an
1101
+ # `RBS::DefinitionBuilder` failure ({#build_instance_definition} / {#build_singleton_definition}'s
1102
+ # rescue) leaves the class KNOWN but with no method surface, so every call on it — real methods and
1103
+ # typos alike — reads `Dynamic[top]` and stops being checkable. `class_known?` consults
1104
+ # {#known_class_names_set}, never a definition build, so nothing downstream can tell the difference.
1105
+ #
1106
+ # NOT forced the way {#env_build_failure} forces `env`, and it MUST NOT be: definition builds are lazy
1107
+ # (ADR-54 WD1 — per class, on first demand), so this answers "which classes has THIS loader failed to
1108
+ # build so far". A reader that forces would have to build every known class, which is a different and
1109
+ # far more expensive question than the run asked. The consequence for callers is a timing contract: a
1110
+ # snapshot taken before the per-file loop reads empty. `Runner::PoolCoordinator` reads it after.
1111
+ #
1112
+ # Recorded, not derived. The two conditions this sits beside are re-derivable from the built env
1113
+ # ({#quarantined_signatures} re-parses; {#virtual_rbs_collision_quarantined} inspects buffers), but a
1114
+ # definition-build failure leaves no trace in the env at all — the env is fine; it is the BUILD over it
1115
+ # that raised — so the rescue is the only place that ever knows.
1116
+ #
1117
+ # @return [Array<Array(String, String, String, Array<String>)>] `[class_name, error_class_name,
1118
+ # first_error_line, conflicting_buffer_names]`, one per class, in first-failure order. Empty for a
1119
+ # healthy sig set, which is the common case.
1120
+ def definition_build_failures
1121
+ (@state[:definition_build_failures] || []).dup.freeze
1122
+ end
1123
+
856
1124
  # Virtual (inline-synthesized) contributions dropped by the collision quarantine
857
1125
  # ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
858
1126
  # entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
@@ -951,8 +1219,25 @@ module Rigor
951
1219
  # silently the way the v0.0.9 cache `Cache::Descriptor` regression did.
952
1220
  end
953
1221
 
1222
+ # Yields every type-alias declaration loaded into the environment (`type foo = ...`).
1223
+ # Yields `(RBS::TypeName, RBS::Environment::TypeAliasEntry)` pairs.
1224
+ def each_type_alias_decl
1225
+ return enum_for(:each_type_alias_decl) unless block_given?
1226
+ return unless env
1227
+
1228
+ env.type_alias_decls.each do |type_name, decl_entry|
1229
+ yield [type_name, decl_entry]
1230
+ end
1231
+ end
1232
+
954
1233
  # ADR-20 slice 2e — iterates over every `%a{...}` annotation attached to a class- or module-level
955
1234
  # declaration in the loaded RBS environment, yielding `(annotation_string, source_location)` pairs.
1235
+ #
1236
+ # Declarations come from {.entry_declarations}, NOT from a direct `entry.each_decl`: that accessor is
1237
+ # RBS 4.x-only, so the direct call raised `NoMethodError` on every RBS 3.x host — inside a rescue that
1238
+ # deliberately does not swallow `NoMethodError`. It stayed latent because nothing the `rbs-compat` job
1239
+ # runs reached an annotated declaration until #672 added a spec under `spec/rigor/environment` that
1240
+ # drives a whole `Runner`.
956
1241
  # Used by {Rigor::Inference::HktRegistry.scan_rbs_loader} to find `rigor:v1:hkt_register` /
957
1242
  # `rigor:v1:hkt_define` directives in user-authored overlays and merge them into the per-`Environment`
958
1243
  # HKT registry. Yields nothing when the env failed to build (fail-soft, same shape as
@@ -962,7 +1247,7 @@ module Rigor
962
1247
  return if env.nil?
963
1248
 
964
1249
  env.class_decls.each_value do |entry|
965
- entry.each_decl do |decl|
1250
+ self.class.entry_declarations(entry).each do |decl|
966
1251
  next unless decl.respond_to?(:annotations)
967
1252
 
968
1253
  decl.annotations.each { |a| yield a.string, a.location }
@@ -983,7 +1268,7 @@ module Rigor
983
1268
  return if env.nil?
984
1269
 
985
1270
  env.class_decls.each do |rbs_name, entry|
986
- entry.each_decl do |decl|
1271
+ self.class.entry_declarations(entry).each do |decl|
987
1272
  next unless decl.respond_to?(:annotations)
988
1273
 
989
1274
  decl.annotations.each { |a| yield rbs_name.to_s, a.string, a.location }
@@ -1029,11 +1314,22 @@ module Rigor
1029
1314
  # keeps the per-process short-circuit. ADR-54 WD1 retired the definitions disk blob: given a cached
1030
1315
  # env, `Marshal.load`-ing every definition was measurably slower (and allocation-heavier) than
1031
1316
  # rebuilding the ones a run actually touches.
1317
+ #
1318
+ # Issue #696 — the report fires HERE, on the result, rather than in `build_*`'s rescue, and it fires on
1319
+ # a memo hit too. `#prewarm`'s cached producers walk every known class through this method, so by the
1320
+ # time a run demands `String` the nil is already memoised and the rescue will never run again; a report
1321
+ # wired to the rescue would say nothing. The guard keeps the cost on the hot path at one `@state` read,
1322
+ # and only on the nil branch — `nil` here is overwhelmingly an unknown class, not a failed build.
1032
1323
  def instance_definition(class_name)
1033
1324
  key = class_name.to_s
1034
- return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
1035
-
1036
- @instance_definition_cache[key] = build_instance_definition(class_name)
1325
+ definition =
1326
+ if @instance_definition_cache.key?(key)
1327
+ @instance_definition_cache[key]
1328
+ else
1329
+ @instance_definition_cache[key] = build_instance_definition(class_name)
1330
+ end
1331
+ report_definition_build_failure(class_name) if definition.nil? && @state[:definition_build_details]
1332
+ definition
1037
1333
  end
1038
1334
 
1039
1335
  # @return [RBS::Definition::Method, nil]
@@ -1091,7 +1387,14 @@ module Rigor
1091
1387
  name = name.absolute! unless name.absolute?
1092
1388
  return nil unless env.type_alias_decls.key?(name)
1093
1389
 
1094
- builder.expand_alias2(name, rbs_alias.args)
1390
+ # Memoized per (name, args): the env is immutable for the loader's lifetime, so the expansion is a
1391
+ # pure function of the pair, and #529's translator wiring re-expands the same handful of aliases
1392
+ # (`Prism::node`, `int`, `string`, …) at every call site — the memo recoups most of that wall cost.
1393
+ memo = (@state[:type_alias_expansions] ||= {})
1394
+ key = [name, rbs_alias.args]
1395
+ return memo[key] if memo.key?(key)
1396
+
1397
+ memo[key] = builder.expand_alias2(name, rbs_alias.args)
1095
1398
  rescue ::RBS::BaseError, StandardError
1096
1399
  nil
1097
1400
  end
@@ -1103,11 +1406,18 @@ module Rigor
1103
1406
  #
1104
1407
  # Built on demand from the env with a per-process memo; the same on-demand discipline as
1105
1408
  # {#instance_definition} (ADR-54 WD1).
1409
+ #
1410
+ # The singleton twin of {#instance_definition}, reporting on the same terms (issue #696).
1106
1411
  def singleton_definition(class_name)
1107
1412
  key = class_name.to_s
1108
- return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
1109
-
1110
- @singleton_definition_cache[key] = build_singleton_definition(class_name)
1413
+ definition =
1414
+ if @singleton_definition_cache.key?(key)
1415
+ @singleton_definition_cache[key]
1416
+ else
1417
+ @singleton_definition_cache[key] = build_singleton_definition(class_name)
1418
+ end
1419
+ report_definition_build_failure(class_name) if definition.nil? && @state[:definition_build_details]
1420
+ definition
1111
1421
  end
1112
1422
 
1113
1423
  # @return [RBS::Definition::Method, nil] the class method on `class_name`. For example,
@@ -1150,6 +1460,47 @@ module Rigor
1150
1460
  @hierarchy.class_ordering(lhs, rhs)
1151
1461
  end
1152
1462
 
1463
+ # The ancestor chain of ONE class, as {RbsHierarchy} asks it to order two classes.
1464
+ #
1465
+ # Issue #696 review (second pass) — this used to live in the hierarchy, which branched on
1466
+ # `cache_store`: with a store it built the whole `RbsClassAncestorTable` and read one key, without a
1467
+ # store it demanded that one class. Three configurations, three different answers to "which classes
1468
+ # failed to build" for one project — 504 classes on a cold store, 0 on a warm one, 2 with no store —
1469
+ # and on the DEFAULT `--workers=0` path, where nothing pre-warms the store first, the cold answer was
1470
+ # the one written into the run-result cache and replayed on every warm run after it.
1471
+ #
1472
+ # The fix is {#during_internal_demand} on BOTH sides, not the removal of the branch. Collapsing to a
1473
+ # per-class demand everywhere was tried first and measured 50x slower on the warm path a cached run
1474
+ # actually takes (200 orderings: 0.0035s reading the table, 0.18s building 400 definitions), which is
1475
+ # the wrong trade for a determinism the marking already buys. What has to be identical across cache
1476
+ # states is the diagnostic, and with both sides marked neither contributes to it: the reported set is
1477
+ # the classes whose METHOD SURFACE the analysis demanded, in every configuration. The memo-hit report
1478
+ # in {#instance_definition} is what makes that hold — a class this path built and memoised as nil is
1479
+ # still reported when a real demand for it arrives later.
1480
+ #
1481
+ # The table side goes through {#ancestor_names_table}, the loader's own marked accessor, rather than
1482
+ # `Cache::RbsClassAncestorTable.fetch` directly. That direct fetch was the bypass: it walked every
1483
+ # known class through the public {#instance_definition} with nothing marking it.
1484
+ #
1485
+ # Answers are identical on both sides — the table's producer computes exactly this, keyed the same
1486
+ # way, and both yield `[]` for an unknown or unbuildable class. Pinned by spec across all three cache
1487
+ # states.
1488
+ #
1489
+ # @return [Array<String>] `::`-stripped ancestor names, or `[]` for an unknown or unbuildable class.
1490
+ def ancestor_names_for(class_name)
1491
+ key = class_name.to_s.delete_prefix("::")
1492
+ during_internal_demand do
1493
+ next ancestor_names_table.fetch(key, [].freeze) if cache_store
1494
+
1495
+ definition = instance_definition(key)
1496
+ next [].freeze if definition.nil?
1497
+
1498
+ definition.ancestors.ancestors.map { |ancestor| ancestor.name.to_s.delete_prefix("::") }.uniq.freeze
1499
+ end
1500
+ rescue ::RBS::BaseError, StandardError
1501
+ [].freeze
1502
+ end
1503
+
1153
1504
  # @return [Array<String>] every RBS-declared constant name (top-level prefixed, e.g., `"::Math::PI"`)
1154
1505
  # currently loaded into the environment. Used by the cache producer that materialises the
1155
1506
  # constant-type table; ordinary callers should keep using {#constant_type} for point lookups.
@@ -1210,16 +1561,23 @@ module Rigor
1210
1561
  # No-op when `cache_store` is nil — without a Store the worker has no choice but to build env via the
1211
1562
  # loader, so the caller MUST ensure pool mode runs with caching enabled. Returns `self` so the call
1212
1563
  # chains cleanly from the `Runner` pre-spawn hook.
1564
+ #
1565
+ # Issue #696 — the whole body is a {#during_internal_demand}, not only the producers that happen to walk
1566
+ # definitions today. On a COLD store `RbsClassTypeParamNames.compute` and `RbsClassAncestorTable.compute`
1567
+ # ask {#instance_definition} for every known class; marking each producer covers that, and marking
1568
+ # `#prewarm` itself covers whichever producer grows the same appetite next.
1213
1569
  def prewarm
1214
1570
  return self if cache_store.nil?
1215
1571
 
1216
- env
1217
- known_class_names_set
1218
- constant_type_table
1219
- type_param_names_table
1220
- ancestor_names_table
1221
- instance_definitions_table
1222
- singleton_definitions_table
1572
+ during_internal_demand do
1573
+ env
1574
+ known_class_names_set
1575
+ constant_type_table
1576
+ type_param_names_table
1577
+ ancestor_names_table
1578
+ instance_definitions_table
1579
+ singleton_definitions_table
1580
+ end
1223
1581
  self
1224
1582
  end
1225
1583
 
@@ -1318,21 +1676,21 @@ module Rigor
1318
1676
  end
1319
1677
 
1320
1678
  def constant_type_table
1321
- @constant_type_table ||= begin
1679
+ @constant_type_table ||= during_internal_demand do
1322
1680
  require_relative "../cache/rbs_constant_table"
1323
1681
  fetch_or_compute_producer(Cache::RbsConstantTable)
1324
1682
  end
1325
1683
  end
1326
1684
 
1327
1685
  def known_class_names_set
1328
- @known_class_names_set ||= begin
1686
+ @known_class_names_set ||= during_internal_demand do
1329
1687
  require_relative "../cache/rbs_known_class_names"
1330
1688
  fetch_or_compute_producer(Cache::RbsKnownClassNames)
1331
1689
  end
1332
1690
  end
1333
1691
 
1334
1692
  def type_param_names_table
1335
- @type_param_names_table ||= begin
1693
+ @type_param_names_table ||= during_internal_demand do
1336
1694
  require_relative "../cache/rbs_class_type_param_names"
1337
1695
  fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
1338
1696
  end
@@ -1352,7 +1710,7 @@ module Rigor
1352
1710
  # the existing `RbsClassAncestorTable` producer when `cache_store` is set; falls back to the
1353
1711
  # producer's `compute` otherwise. Used by {#reflection}.
1354
1712
  def ancestor_names_table
1355
- @ancestor_names_table ||= begin
1713
+ @ancestor_names_table ||= during_internal_demand do
1356
1714
  require_relative "../cache/rbs_class_ancestor_table"
1357
1715
  fetch_or_compute_producer(Cache::RbsClassAncestorTable)
1358
1716
  end
@@ -1509,15 +1867,20 @@ module Rigor
1509
1867
  # all definitions from a cached env is faster), so the eager-table cost is now a compute, not a load.
1510
1868
  # Keys stay in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`) — the shape
1511
1869
  # {Environment::Reflection} documents.
1870
+ #
1871
+ # {#during_internal_demand} (issue #696) — these two walk EVERY known class, so a failure they hit is a
1872
+ # failure of the sig set, not of anything the run asked about, and it must not reach
1873
+ # {#definition_build_failures}. The stderr banner is deliberately left armed here, byte-for-byte as
1874
+ # before.
1512
1875
  def instance_definitions_table
1513
- @state[:instance_definitions_table] ||= build_definitions_table do |name|
1514
- build_instance_definition(name)
1876
+ @state[:instance_definitions_table] ||= during_internal_demand do
1877
+ build_definitions_table { |name| build_instance_definition(name) }
1515
1878
  end
1516
1879
  end
1517
1880
 
1518
1881
  def singleton_definitions_table
1519
- @state[:singleton_definitions_table] ||= build_definitions_table do |name|
1520
- build_singleton_definition(name)
1882
+ @state[:singleton_definitions_table] ||= during_internal_demand do
1883
+ build_definitions_table { |name| build_singleton_definition(name) }
1521
1884
  end
1522
1885
  end
1523
1886
 
@@ -1538,7 +1901,8 @@ module Rigor
1538
1901
  self.class.build_env_for(
1539
1902
  libraries: @libraries,
1540
1903
  signature_paths: @signature_paths,
1541
- virtual_rbs: @virtual_rbs
1904
+ virtual_rbs: @virtual_rbs,
1905
+ deferred_signature_paths: @deferred_signature_paths
1542
1906
  )
1543
1907
  end
1544
1908
 
@@ -1552,6 +1916,7 @@ module Rigor
1552
1916
 
1553
1917
  builder.build_instance(rbs_name)
1554
1918
  rescue ::RBS::BaseError => e
1919
+ store_definition_build_detail(class_name, e)
1555
1920
  warn_about_definition_build_failure(class_name, e)
1556
1921
  nil
1557
1922
  end
@@ -1566,10 +1931,94 @@ module Rigor
1566
1931
 
1567
1932
  builder.build_singleton(rbs_name)
1568
1933
  rescue ::RBS::BaseError => e
1934
+ store_definition_build_detail(class_name, e)
1569
1935
  warn_about_definition_build_failure(class_name, e)
1570
1936
  nil
1571
1937
  end
1572
1938
 
1939
+ # Issue #696 — the DETAIL half of the same rescue: what went wrong for this class, remembered so
1940
+ # {#definition_build_failures} can report it. Split from the REPORTING decision on purpose, and the
1941
+ # split is the whole fix.
1942
+ #
1943
+ # Recording in the rescue is wrong in both directions at once. Too loud: `#prewarm` and the cached
1944
+ # table producers walk EVERY known class, so on a cold store they enter this rescue for every class a
1945
+ # collision took down — 1,336 of them for one `class Object` duplicate — and the diagnostic named all
1946
+ # of them under `--workers=N` with a cold cache while naming 3 under every other configuration. Too
1947
+ # quiet: {#instance_definition} memoises the nil, so once that walk has run, the FIRST real demand
1948
+ # never re-enters the rescue and would record nothing at all.
1949
+ #
1950
+ # So the rescue only remembers, keyed by the `::`-stripped name (the eager walk spells it `::Acme`,
1951
+ # a dispatch site spells it `Acme`, and they are one class — the {#synthesized_namespaces}
1952
+ # convention), and {#report_definition_build_failure} decides. First writer wins: the instance and
1953
+ # singleton sides fail for the same underlying collision, and the first is the one a caller hit.
1954
+ def store_definition_build_detail(class_name, error)
1955
+ key = class_name.to_s.delete_prefix("::")
1956
+ details = (@state[:definition_build_details] ||= {})
1957
+ return if details.key?(key)
1958
+
1959
+ details[key] = [key, error.class.name.to_s, definition_build_member(error),
1960
+ definition_build_conflict_buffers(error)].freeze
1961
+ end
1962
+
1963
+ # Issue #696 — the REPORTING half: promote a remembered detail into {#definition_build_failures}
1964
+ # because something the run was actually doing asked for this class's definition and got nothing.
1965
+ #
1966
+ # Called from {#instance_definition} / {#singleton_definition} — the demand entries — rather than from
1967
+ # the rescue, so it fires on a MEMO HIT too. That is what survives a pre-warm: the walk builds the
1968
+ # definition, fails, memoises nil, and never enters the rescue again, but the next real demand still
1969
+ # reads nil here and still reports. Reporting from the rescue could only ever see the first build.
1970
+ #
1971
+ # Silent inside a whole-universe walk ({#during_internal_demand}): a producer that touches every known
1972
+ # class is asking a question the RUN did not ask, and letting it contribute makes the reported class
1973
+ # list depend on whether a cache was cold — the same project saying different things under
1974
+ # `--workers=N` than under `--workers=0`, which is the defect this diagnostic exists to end.
1975
+ def report_definition_build_failure(class_name)
1976
+ return if @state[:internal_demand]
1977
+
1978
+ details = @state[:definition_build_details]
1979
+ return if details.nil?
1980
+
1981
+ key = class_name.to_s.delete_prefix("::")
1982
+ detail = details[key]
1983
+ return if detail.nil?
1984
+
1985
+ reported = (@state[:definition_build_reported] ||= {})
1986
+ return if reported[key]
1987
+
1988
+ reported[key] = true
1989
+ (@state[:definition_build_failures] ||= []) << detail
1990
+ end
1991
+
1992
+ # Marks a definition demand that is RIGOR'S OWN, not the analysed program's, so
1993
+ # {#report_definition_build_failure} stays silent for the duration.
1994
+ #
1995
+ # The diagnostic reports classes whose METHOD SURFACE the analysis asked to resolve — that is what its
1996
+ # message promises, and it is the only demand set that does not vary with how the run was invoked. Two
1997
+ # kinds of demand are Rigor's own and must be excluded:
1998
+ #
1999
+ # - a whole-universe cache producer ({#prewarm}'s tables), which asks about every known class;
2000
+ # - the hierarchy oracle's ancestry lookup ({#ancestor_names_for}), which asks whether two classes are
2001
+ # ordered, not whether either one's methods resolve.
2002
+ #
2003
+ # Both were cache-state-dependent before this, and each produced a DIFFERENT class list per
2004
+ # configuration for the same project — the second one on the DEFAULT `--workers=0` path (issue #696
2005
+ # review, second pass).
2006
+ #
2007
+ # 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.
2009
+ #
2010
+ # Per LOADER, not per thread. Nesting and a raise mid-demand are both handled, and the fork pool forks
2011
+ # after `#prewarm` returns, so no CLI path shares a loader across concurrent analyses. An in-process
2012
+ # host that did (`language_server/debouncer.rb` runs analysis on a `Thread`) could have one analysis
2013
+ # silence another's reporting; not reachable today, and not worth a thread-local until it is.
2014
+ def during_internal_demand
2015
+ previous = @state[:internal_demand]
2016
+ @state[:internal_demand] = true
2017
+ yield
2018
+ ensure
2019
+ @state[:internal_demand] = previous
2020
+ end
2021
+
1573
2022
  # The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
1574
2023
  # once per class per PROCESS, a `RBS::DefinitionBuilder` failure caught in
1575
2024
  # {#build_instance_definition} / {#build_singleton_definition}'s rescue. Without it, `class_known?`
@@ -1624,22 +2073,53 @@ module Rigor
1624
2073
  # `#location`. Falls back to an empty list — the warning still names the class and the exception's own
1625
2074
  # message — for the remainder (e.g. `SuperclassMismatchError`, which carries neither).
1626
2075
  def definition_build_conflict_buffers(error)
1627
- locations =
1628
- if error.respond_to?(:members) && error.members
1629
- Array(error.members).map(&:location)
1630
- elsif error.respond_to?(:member) && error.member
1631
- [error.member.location]
1632
- elsif error.respond_to?(:location)
1633
- [error.location]
1634
- else
1635
- []
1636
- end
1637
-
1638
- locations.compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s).uniq.freeze
2076
+ definition_build_error_locations(error)
2077
+ .compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s)
2078
+ .reject { |name| name == CACHED_LOCATION_BUFFER_NAME }.uniq.freeze
1639
2079
  rescue ::RBS::BaseError, StandardError
1640
2080
  [].freeze
1641
2081
  end
1642
2082
 
2083
+ # The `RBS::Location`s an error carries, by whichever accessor its class happens to expose (see the
2084
+ # shape survey above). Split out so the buffer-name extraction reads as one pipeline.
2085
+ def definition_build_error_locations(error)
2086
+ return Array(error.members).map(&:location) if error.respond_to?(:members) && error.members
2087
+ return [error.member.location] if error.respond_to?(:member) && error.member
2088
+ return [error.location] if error.respond_to?(:location)
2089
+
2090
+ []
2091
+ end
2092
+
2093
+ # The member the failure is ABOUT, taken off the error object rather than parsed out of its message.
2094
+ #
2095
+ # Issue #696 — the message cannot serve here. It is built from `RBS::Location`s, and the ADR-54 env
2096
+ # cache drops their POSITIONS, so a warm run's message would name no line and the same project would
2097
+ # report different text cold and warm. These accessors are `RBS::TypeName`s and Symbols, so they
2098
+ # survive the marshal round trip unchanged — and they name the class that actually CARRIES the
2099
+ # duplicate, which the failed-class list does not: a collision on `::Object#blank?` fails `String`,
2100
+ # `Integer` and `Array`, none of which is where the fix goes. (The buffer NAME does survive, since the
2101
+ # second review pass; it is the file list that uses it, not this.)
2102
+ #
2103
+ # Shapes vary by error class (`references/rbs`'s `lib/rbs/errors.rb`): the two duplicated-definition
2104
+ # errors expose `#qualified_method_name` directly; `InvalidOverloadMethodError` has `#type_name` +
2105
+ # `#method_name`; `UnknownMethodAliasError` and `NoSuperclassFoundError` have `#type_name`;
2106
+ # `SuperclassMismatchError` has `#name`. `RecursiveAncestorError` has none of them and yields nil, and
2107
+ # the diagnostic then omits the clause rather than inventing one.
2108
+ #
2109
+ # @return [String, nil]
2110
+ def definition_build_member(error)
2111
+ return error.qualified_method_name.to_s if error.respond_to?(:qualified_method_name)
2112
+
2113
+ type_name = error.respond_to?(:type_name) ? error.type_name : nil
2114
+ type_name ||= error.respond_to?(:name) ? error.name : nil
2115
+ return nil if type_name.nil?
2116
+
2117
+ method_name = error.respond_to?(:method_name) ? error.method_name : nil
2118
+ method_name.nil? ? type_name.to_s : "#{type_name}##{method_name}"
2119
+ rescue ::RBS::BaseError, StandardError
2120
+ nil
2121
+ end
2122
+
1643
2123
  # Resolve an RBS class/module ALIAS to its canonical declared name. `class Mutex = Thread::Mutex`
1644
2124
  # lives only in `class_alias_decls`, so `class_known?` reports it (it checks that table) but the
1645
2125
  # definition builder — which only knows `class_decls` — could not enumerate its methods, leaving alias
@@ -1688,6 +2168,5 @@ module Rigor
1688
2168
  false
1689
2169
  end
1690
2170
  end
1691
- # rubocop:enable Metrics/ClassLength
1692
2171
  end
1693
2172
  end