rigortype 0.4.7 → 0.4.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.
Files changed (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -64,8 +64,10 @@ module Rigor
64
64
 
65
65
  # ADR-89 WD2 — bounds on the persisted return summaries (per-def observed call keys, and total defs
66
66
  # summarised) so the incremental snapshot stays small. A def observed under more keys than the per-def
67
- # cap keeps only the first; a def past the total cap is dropped (its symbol dependents then always
68
- # re-check — the conservative direction).
67
+ # cap keeps none of them (issue #3116): the keys past the cap are unknown, so equal returns at the first
68
+ # eight prove nothing about the ninth key's reader. A def past the total cap keeps none either. Both are
69
+ # harvested as nil ({#return_summaries}), which the session stores as no summary at all, so the def's symbol
70
+ # dependents always re-check — the conservative direction.
69
71
  RETURN_SUMMARY_KEYS_PER_DEF = 8
70
72
  RETURN_SUMMARY_TOTAL_CAP = 4000
71
73
 
@@ -515,6 +517,10 @@ module Rigor
515
517
  # The return memo bucket the last `run` filled, kept for {#return_summaries} once the run has released the
516
518
  # thread's slot; nil before the first run.
517
519
  @return_memo = nil
520
+ # The `[path, node_id] => Prism::DefNode` table the last `run`'s {Inference::DefNodeResolver} filled, kept for
521
+ # the same reader.
522
+ @resolved_def_nodes = nil
523
+ @probe_reads = {}.freeze
518
524
  build_collaborators
519
525
  end
520
526
 
@@ -528,6 +534,9 @@ module Rigor
528
534
  # errors. The Environment is built once at run start through `Environment.for_project` so all files
529
535
  # share the same RBS load.
530
536
  def run(paths = @configuration.paths)
537
+ # ADR-129 WD5 (#2785) — a mismatch left on the thread by typing after an earlier run (its return summaries, a
538
+ # language-server hover) fails here rather than reaching this run's first file as a crash row.
539
+ Inference::MemoAudit.raise_undrained!
531
540
  # One per-run file-digest memo spans the whole run, so a path is SHA-256'd at most once across the
532
541
  # run-diagnostics dependency descriptor, its `fresh?` validation, the RBS signature tree, and every
533
542
  # plugin producer's watched-glob validation (they overlap heavily on the warm path). The nested ADR-85
@@ -536,7 +545,12 @@ module Rigor
536
545
  # ADR-87 WD1 — `cache.validation` (or the RIGOR_STRICT_VALIDATION env, which wins) selects the
537
546
  # freshness path for this run; the `auto` default resolves strict in CI (#190), stat-first elsewhere.
538
547
  Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
539
- Inference::DefNodeResolver.with_run { run_analysis(paths) }
548
+ Inference::DefNodeResolver.with_run do
549
+ run_analysis(paths)
550
+ ensure
551
+ # Issue #3160 — the memo keys an unchanged file's def on the node the resolver minted for its handle.
552
+ @resolved_def_nodes = Inference::DefNodeResolver.resolved_nodes
553
+ end
540
554
  ensure
541
555
  # ADR-89 WD2 — {#return_summaries} reads the return memo after the run, so the runner keeps the bucket the
542
556
  # run filled before the thread's run-scoped slots are released (issues #2412, #2399).
@@ -788,15 +802,28 @@ module Rigor
788
802
  end
789
803
 
790
804
  # ADR-89 WD2 — the per-method observed-key return summaries the run's ADR-84 return memo just captured.
791
- # For each project method with live memo entries, a bounded `{ keys:, returns:, effects: }` summary the
792
- # incremental session persists: `keys` the observed `[receiver, arg_types]` type tuples, `returns` their
805
+ # For each project method the run observed, a bounded `{ keys:, returns:, effects:, guard_constants: }` summary
806
+ # the incremental session persists: `keys` the observed `[receiver, arg_types]` type tuples, `returns` their
793
807
  # `describe(:short)` descriptors, `effects` the content-mutated parameter positions (a caller-visible
794
- # arg-flooring surface). Only meaningful right after a run populated the memo (baseline / recheck),
808
+ # arg-flooring surface), `guard_constants` the published-constant spellings its body reads ({#guard_constants}).
809
+ # Only meaningful right after a run populated the memo (baseline / recheck),
795
810
  # before the bucket rolls. Keys are held live here; the session Marshals them for the snapshot.
796
811
  #
812
+ # Issue #3160 — the session reads the harvest as EVERY key the files this run analysed observed each def
813
+ # under, so whatever cannot be that says so instead of looking like a short key list:
814
+ #
815
+ # - a def observed under keys this run cannot list maps to nil: more keys than
816
+ # {RETURN_SUMMARY_KEYS_PER_DEF} (issue #3116), a call whose result the memo refused to store, or a def
817
+ # past {RETURN_SUMMARY_TOTAL_CAP};
818
+ # - a def absent from the result was observed by no analysed file, wherever it is defined: an unchanged
819
+ # file's def is found through the node its {Inference::DefHandle} resolved to;
820
+ # - the whole result is nil for a pool run, whose workers typed the calls in memos this process never sees.
821
+ #
797
822
  # @return the summaries keyed by `[path, "Class#method"]` (or `"Class.method"`), each holding `keys:`,
798
- # `returns:` and `effects:`.
823
+ # `returns:` and `effects:`, or nil; nil for a pool run.
799
824
  def return_summaries
825
+ return nil if pool_mode?
826
+
800
827
  memo = Inference::ExpressionTyper.harvest_return_memo(@return_memo)
801
828
  return {} if memo.empty?
802
829
 
@@ -806,37 +833,89 @@ module Rigor
806
833
  "#", memo, effects_evaluator)
807
834
  collect_return_summaries(result, @project_discovered_singleton_def_nodes,
808
835
  @project_discovered_singleton_def_sources, ".", memo, effects_evaluator)
836
+ # ADR-129 WD5 (#2785) — typing after the run, under `content_effects`' rescue.
837
+ Inference::MemoAudit.raise_undrained!
809
838
  result
810
839
  end
811
840
 
812
841
  # Folds one def-node/def-source table pair's memo entries into the return-summary result under
813
- # `separator`. A live def node (a cold / re-walked file) is the identity every cross-file caller
814
- # resolved through, so `memo[node]` holds those callers' observed keys; a {DefHandle} (an unchanged
815
- # file on the warm path) never keyed a memo entry, so it is skipped (its summary is carried over from
816
- # the snapshot). Bounded per def and in total so the snapshot stays small.
842
+ # `separator`. The node the memo keyed is the identity every cross-file caller resolved through: the live
843
+ # def node of a cold / re-walked file, or the one the resolver minted for an unchanged file's
844
+ # {Inference::DefHandle} ({#observed_def_node}).
817
845
  def collect_return_summaries(result, nodes, sources, separator, memo, effects_evaluator)
846
+ summarised = result.count { |_pair, summary| summary }
818
847
  nodes.each do |class_name, methods|
819
- methods.each do |method_name, node|
820
- next if node.is_a?(Inference::DefHandle)
821
-
822
- entries = memo[node]
823
- next if entries.nil? || entries.empty?
848
+ methods.each do |method_name, slot|
849
+ node = observed_def_node(slot)
850
+ next unless memo.key?(node)
824
851
 
825
852
  path_line = sources.dig(class_name, method_name)
826
853
  next if path_line.nil?
827
854
 
828
- break if result.size >= RETURN_SUMMARY_TOTAL_CAP
829
-
830
- capped = entries.first(RETURN_SUMMARY_KEYS_PER_DEF)
831
- result[[path_line.split(":", 2).first, "#{class_name}#{separator}#{method_name}"]] = {
832
- keys: capped.map { |entry| [entry.receiver, entry.arg_types] },
833
- returns: capped.map { |entry| entry.result.describe(:short) },
834
- effects: content_effects(effects_evaluator, node)
835
- }
855
+ summary = summarised < RETURN_SUMMARY_TOTAL_CAP ? return_summary(memo[node], effects_evaluator, node) : nil
856
+ summarised += 1 if summary
857
+ result[[path_line.split(":", 2).first, "#{class_name}#{separator}#{method_name}"]] = summary
836
858
  end
837
859
  end
838
860
  end
839
861
 
862
+ # The node the run's return memo keyed `slot`'s def on, or nil when no call resolved it: the live node, or the
863
+ # one the run's resolver minted for a {Inference::DefHandle}.
864
+ def observed_def_node(slot)
865
+ return slot unless slot.is_a?(Inference::DefHandle)
866
+
867
+ @resolved_def_nodes&.[]([slot.path, slot.node_id])
868
+ end
869
+
870
+ # One def's summary, or nil when `entries` are not every key it was observed under: the memo refused to store
871
+ # a call of it (so `entries` may be empty), or there are more than {RETURN_SUMMARY_KEYS_PER_DEF} (issue
872
+ # #3116). A splat call's key (issue #1237) is an entry like any other and counts toward the cap.
873
+ def return_summary(entries, effects_evaluator, node)
874
+ return nil if entries.empty? || entries.size > RETURN_SUMMARY_KEYS_PER_DEF
875
+ return nil if Inference::ExpressionTyper.return_memo_refused?(@return_memo, node)
876
+
877
+ { keys: entries.map { |entry| return_summary_key(entry) },
878
+ returns: entries.map { |entry| entry.result.describe(:short) },
879
+ effects: content_effects(effects_evaluator, node),
880
+ guard_constants: guard_constants(node) }
881
+ end
882
+
883
+ # Issue #3186 — the constant spellings in `node`'s body that the truthiness rule's one interprocedural hop
884
+ # ({CheckRules::PublishedConstantGuard}) could answer true for, or nil when they cannot be listed. A reader's
885
+ # `flow.always-truthy-condition` withholds when a def it calls reads a constant another file published,
886
+ # whatever the def returns, so the ADR-89 WD2 gate compares these as well as the returns; nil never compares
887
+ # equal, so it keeps the def's readers.
888
+ #
889
+ # The hop asks `Scope#published_constant?`, which answers true only for a spelling whose last segment the
890
+ # project publishes or the reader's own file assigns from a published constant, and each of those is a segment
891
+ # some project file writes. So the spellings whose last segment no file writes (a class name, as a rule) are
892
+ # left out: they answer false for every reader, and keeping them would fail the gate on every edit that adds a
893
+ # callee. The filter is this run's write census, so a census that moved fails the comparison.
894
+ def guard_constants(node)
895
+ written = constant_write_segments
896
+ CheckRules::PublishedConstantGuard.body_constant_spellings(node.body).select do |spelling|
897
+ written.include?(spelling.split("::").last)
898
+ end
899
+ rescue StandardError
900
+ nil
901
+ end
902
+
903
+ # The last segments of every constant name the run's write census holds, a wildcard key's included.
904
+ def constant_write_segments
905
+ unless @constant_write_segments_of.equal?(@project_constant_writes)
906
+ @constant_write_segments_of = @project_constant_writes
907
+ @constant_write_segments = @project_constant_writes.each_key.to_set { |name| name.split("::").last }
908
+ end
909
+ @constant_write_segments
910
+ end
911
+
912
+ # One observed call key: `[receiver, arg_types]`, or, for an entry a splat call stored (issue #1237),
913
+ # `[receiver, arg_types, true]`, which {#evaluate_spec_returns} re-drives with no positional correspondence.
914
+ def return_summary_key(entry)
915
+ key = [entry.receiver, entry.arg_types]
916
+ entry.positionals_unknown ? key << true : key
917
+ end
918
+
840
919
  # The content-mutated parameter positions of `node`, or `[]` if the computation raises (defensive: the
841
920
  # effects surface only routes attention, never soundness — a missing set reads as "no floor").
842
921
  def content_effects(effects_evaluator, node)
@@ -859,13 +938,45 @@ module Rigor
859
938
  def evaluate_return_types(paths, specs)
860
939
  return {} if specs.empty?
861
940
 
941
+ @probe_reads = {}
942
+ @probe_plugin_reads = {}
943
+ @probe_disclosures = {}
944
+ @probe_guard_constants = {}
862
945
  Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
863
- Inference::DefNodeResolver.with_run { evaluate_return_types_setup(paths, specs) }
946
+ Inference::DefNodeResolver.with_run do
947
+ evaluate_return_types_setup(paths, specs)
948
+ ensure
949
+ @resolved_def_nodes = Inference::DefNodeResolver.resolved_nodes
950
+ end
864
951
  end
865
952
  ensure
953
+ # Issue #3180 — as {#run} does: {#return_summaries} then answers for what the probe observed.
954
+ @return_memo = Inference::ExpressionTyper.current_return_memo
866
955
  RunScopedSlots.release
867
956
  end
868
957
 
958
+ # Issue #3180 — what the last {#evaluate_return_types} read while re-evaluating each spec's def at its keys: the
959
+ # spec triple => a {DependencyRecorder::Record} of every def, class and name the evaluation read, the def's own
960
+ # file included (nothing is filtered as a self-read). The incremental session gives these edges to the readers
961
+ # a passing gate skips, since they now reach whatever the edited def reaches. Empty before the first probe.
962
+ attr_reader :probe_reads
963
+
964
+ # Issue #3187 — what the plugins did while the last {#evaluate_return_types} re-evaluated each spec's def, as
965
+ # {#analysis_reads} and {#analysis_disclosures} hold for an analysed file: the spec triple => the boundary rows
966
+ # its evaluation read ({Plugin::IoBoundary.attributing}), and => the run-scoped rows it registered
967
+ # ({RunDisclosureCredit}). A triple whose evaluation read or registered nothing has no key. The incremental
968
+ # session credits the reads to the readers a passing gate skips, as it does {#probe_reads}; a registration
969
+ # keeps the def's readers instead, since no skipped reader could be credited with it, or have it retracted.
970
+ attr_reader :probe_plugin_reads, :probe_disclosures
971
+
972
+ # Issue #3186 — the spec triple => {#guard_constants} of the def the last {#evaluate_return_types} re-evaluated,
973
+ # under that probe's write census. A triple whose def the probe did not find has no key.
974
+ attr_reader :probe_guard_constants
975
+
976
+ # The consumer {#evaluate_spec_returns} records under. It is no file's path, so no read is dropped as a self-read.
977
+ PROBE_CONSUMER = ""
978
+ private_constant :PROBE_CONSUMER
979
+
869
980
  # Builds the discovery index + environment (a real run's prologue, minus per-file analysis) and
870
981
  # re-evaluates each spec's def. Extracted so {#evaluate_return_types}'s `with_run` wrappers stay thin.
871
982
  def evaluate_return_types_setup(paths, specs)
@@ -901,10 +1012,43 @@ module Rigor
901
1012
  return spec[:keys].map { nil } if node.nil?
902
1013
 
903
1014
  scope = seed_project_scope(Scope.empty(environment: environment, source_path: spec[:path]))
904
- spec[:keys].map do |(receiver, arg_types)|
905
- result = scope.user_method_return(node, receiver, arg_types)
906
- result&.describe(:short)
1015
+ triple = [spec[:class_name], spec[:method_name], spec[:singleton]]
1016
+ @probe_guard_constants[triple] = guard_constants(node)
1017
+ returns = nil
1018
+ attributed_probe(triple) do
1019
+ record_guard_spelling_edges(node)
1020
+ returns = spec[:keys].map do |(receiver, arg_types, positionals_unknown)|
1021
+ result = scope.user_method_return(node, receiver, arg_types,
1022
+ positionals_unknown: positionals_unknown || false)
1023
+ result&.describe(:short)
1024
+ end
1025
+ end
1026
+ returns
1027
+ end
1028
+
1029
+ # Issue #3199 — the `constant:` edge for every spelling the truthiness guard's hop would scan in `node`'s body,
1030
+ # recorded into the probe's reads, which every reader the gate skips receives. {#guard_constants} leaves out a
1031
+ # spelling whose last segment no file writes, so an edit that starts probing one (`defined?(B::X)` before any
1032
+ # file assigns `B::X`) passes the gate; the skipped reader's guard never asks about it, and without this edge a
1033
+ # file that later publishes the constant would re-check nobody.
1034
+ def record_guard_spelling_edges(node)
1035
+ CheckRules::PublishedConstantGuard.body_constant_spellings(node.body).each do |spelling|
1036
+ DependencyRecorder.read_last_segment(:constant, spelling)
1037
+ end
1038
+ end
1039
+
1040
+ # Runs one spec's evaluation with its dependency reads recorded ({#probe_reads}) and its plugin reads and
1041
+ # run-scoped registrations attributed to it ({#probe_plugin_reads}, {#probe_disclosures}), as
1042
+ # {#analyze_file_attributed} does for a file.
1043
+ def attributed_probe(triple, &)
1044
+ reads = []
1045
+ disclosures = RunDisclosureCredit.collect do
1046
+ @probe_reads[triple] = Plugin::IoBoundary.attributing(reads) do
1047
+ DependencyRecorder.record_for(PROBE_CONSUMER, &)
1048
+ end
907
1049
  end
1050
+ @probe_plugin_reads[triple] = reads.uniq.freeze unless reads.empty?
1051
+ @probe_disclosures[triple] = disclosures unless disclosures.empty?
908
1052
  end
909
1053
 
910
1054
  # ADR-45 — unchanged-project fast path. Serves the whole run's (pre-severity-profile) diagnostics
@@ -920,7 +1064,10 @@ module Rigor
920
1064
  # #392 — `source_files:` is what a plugin's `source_rbs_synthesizer` is offered at env-build time,
921
1065
  # and a template unit is not Ruby the synthesiser can read (rbs-inline would be handed `.rbx`
922
1066
  # bytes). The `.rb` expansion is the right set here; the units join the ANALYSED set only.
923
- environment = @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
1067
+ # Issue #2883 — not eager: a warm result-cache HIT never types, so it must not build the RBS environment;
1068
+ # {#assemble_run_diagnostics} builds it on the analysis path.
1069
+ environment = @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files),
1070
+ eager: false)
924
1071
  # Lazy-files descriptor: the cache KEY reads only `gems` + `configs`; the RBS signature-tree `files`
925
1072
  # are digested solely by `run_dependency_descriptor` on a MISS, so a warm HIT never walks the tree.
926
1073
  rbs_descriptor = if environment&.rbs_loader
@@ -1194,6 +1341,8 @@ module Rigor
1194
1341
  end
1195
1342
 
1196
1343
  def assemble_run_diagnostics(expansion, environment: nil)
1344
+ # Issue #2883 — the analysis path builds the RBS environment before anything types (ADR-129 G1').
1345
+ environment&.prepare_for_typing!
1197
1346
  # Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
1198
1347
  # eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
1199
1348
  # `assemble_run_diagnostics`, so it never runs the two whole-project parse passes. Runs over the FULL
@@ -1442,7 +1591,7 @@ module Rigor
1442
1591
  # #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
1443
1592
  # `.rbs` files that existed while the run read them, so without these a signature file written after
1444
1593
  # the run left the slot validating fresh and the warm run answered without it.
1445
- globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries
1594
+ globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries + bundle_sig_glob_entries
1446
1595
  boundary = plugin_boundary_rows
1447
1596
  entries.concat(boundary.files)
1448
1597
  globs.concat(boundary.globs).concat(producer_watch_glob_entries)
@@ -1590,6 +1739,9 @@ module Rigor
1590
1739
  return nil if pool_mode? || !@collect_stats || @analyzed_files.empty?
1591
1740
 
1592
1741
  own, pinned = signature_rows
1742
+ # Issue #2987 — the bundle's `sig/` listing is identified by the gems' directory, outside the project, so a
1743
+ # `pinned` row: the write guard passes over it where it would refuse an `owned` one it cannot stat.
1744
+ pinned = Cache::Descriptor.new(files: pinned.files, globs: (pinned.globs + bundle_sig_glob_entries).uniq)
1593
1745
  BaselineRows.new(
1594
1746
  owned: Cache::Descriptor.new(
1595
1747
  files: own.files + kind_entries(signature_root_candidates) +
@@ -1618,12 +1770,12 @@ module Rigor
1618
1770
  roots.any? { |root| directory.start_with?(root) }
1619
1771
  end
1620
1772
 
1621
- # The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
1622
- # `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
1623
- # signature rows' listing rows cover a file appearing under a root that existed; only a row for the root
1624
- # itself covers it appearing.
1773
+ # The project's own signature roots, whether or not they exist:
1774
+ # {Environment.project_signature_root_candidates} — the configured `signature_paths:`, or the `sig`
1775
+ # directory it defaults to when none are configured (issue #1619). The signature rows' listing rows
1776
+ # cover a file appearing under a root that existed; only a row for the root itself covers it appearing.
1625
1777
  def signature_root_candidates
1626
- @configuration.signature_paths || ["sig"]
1778
+ Environment.project_signature_root_candidates(@configuration.signature_paths)
1627
1779
  end
1628
1780
 
1629
1781
  # One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
@@ -1638,6 +1790,22 @@ module Rigor
1638
1790
  end
1639
1791
  end
1640
1792
 
1793
+ # Issue #2987 — one names-mode listing row per bundle `sig/` layout ({Environment::BundleSigDiscovery.discover}'s
1794
+ # two globs). The signature rows list only the directories the loader read, so a gem that GAINS a `sig/`
1795
+ # (or loses it) while its version and lock entry stand still sits in none of them, and the run-result
1796
+ # slot kept serving the environment built without (or with) it.
1797
+ def bundle_sig_glob_entries
1798
+ root = Environment::BundleSigDiscovery.resolve_bundle_path(
1799
+ bundle_path: @configuration.bundler_bundle_path, project_root: Dir.pwd,
1800
+ auto_detect: @configuration.bundler_auto_detect
1801
+ )
1802
+ return [] if root.nil?
1803
+
1804
+ Environment::BundleSigDiscovery::SIG_GLOBS.map do |pattern|
1805
+ Cache::Descriptor::GlobEntry.compute(root: root.to_s, pattern: pattern, mode: :names)
1806
+ end
1807
+ end
1808
+
1641
1809
  def unanalysed_pre_eval_entries(files)
1642
1810
  analysed = files.to_set { |path| File.absolute_path(path) }
1643
1811
  outside = @configuration.pre_eval.reject { |path| analysed.include?(File.absolute_path(path)) }
@@ -1658,6 +1826,7 @@ module Rigor
1658
1826
  end
1659
1827
  private :plugin_boundary_rows, :producer_watch_glob_entries, :kind_entries, :root_presence_entries,
1660
1828
  :analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
1829
+ :bundle_sig_glob_entries,
1661
1830
  :unanalysed_pre_eval_entries, :signature_rows, :within?
1662
1831
 
1663
1832
  # Issue #684 — the project files this run DISCOVERED but did not analyse. A widened run's diagnostics
@@ -2031,7 +2200,7 @@ module Rigor
2031
2200
  rescue ArgumentError => e
2032
2201
  floor = self.class.prism_supported_floor
2033
2202
  Diagnostic.new(
2034
- path: ".rigor.yml", line: 1, column: 1,
2203
+ path: @configuration.config_display_path, line: 1, column: 1,
2035
2204
  message: "target_ruby #{@configuration.target_ruby.inspect} is not supported by this Rigor build " \
2036
2205
  "(Prism accepts #{floor} and newer). Set target_ruby to your project's Ruby version " \
2037
2206
  "(>= #{floor}) — read it from Gemfile.lock's `RUBY VERSION` or .ruby-version. " \
@@ -2661,6 +2830,14 @@ module Rigor
2661
2830
  # `DependencyRecorder.active? == false`. Recording is observational, so diagnostics are byte-identical
2662
2831
  # either way.
2663
2832
  def analyze_file(path, environment)
2833
+ diagnostics = analyze_file_attributed(path, environment)
2834
+ return diagnostics unless Inference::MemoAudit.enabled?
2835
+
2836
+ # ADR-129 WD5 (#2785) — the audit's mismatches this file raised, a swallowed one included.
2837
+ CrashSignature.with_swallowed(path, diagnostics, Inference::MemoAudit.drain)
2838
+ end
2839
+
2840
+ def analyze_file_attributed(path, environment)
2664
2841
  return analyze_with_effects(path, environment) unless @record_dependencies
2665
2842
 
2666
2843
  # ADR-45 WD2 (#1507) — every plugin read this file's analysis makes is attributed to it, so the
@@ -2741,9 +2918,11 @@ module Rigor
2741
2918
  # `self_call_misses` feeds the `call.self-undefined-method` collector; the recorder is inert unless
2742
2919
  # the rule is active or `record_self_calls:` opted in.
2743
2920
  index = nil
2921
+ TypeOfMemo.begin_file
2744
2922
  self_call_record = with_self_call_recording(path) do
2745
2923
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope, widen_clauses: true)
2746
2924
  end
2925
+ TypeOfMemo.phase!(:rule)
2747
2926
  self_call_misses = self_call_record ? self_call_record.calls : []
2748
2927
  diagnostics = rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
2749
2928
  notify_file_indexed(path, parse_result, scope, index)
@@ -2758,16 +2937,18 @@ module Rigor
2758
2937
  severity: :error
2759
2938
  )
2760
2939
  ]
2761
- rescue StandardError => e
2940
+ rescue StandardError, SystemStackError => e
2762
2941
  [
2763
2942
  Diagnostic.new(
2764
2943
  path: path,
2765
2944
  line: 1,
2766
2945
  column: 1,
2767
- message: CrashSignature.check_rule_message(e),
2946
+ message: CrashSignature.contained_message!(e, parse_result),
2768
2947
  severity: :error
2769
2948
  )
2770
2949
  ]
2950
+ ensure
2951
+ TypeOfMemo.end_file
2771
2952
  end
2772
2953
 
2773
2954
  # ADR-113 WD2 — the `on_file_indexed` observer. It reads; an error inside it never changes this file's
@@ -2784,7 +2965,9 @@ module Rigor
2784
2965
  # output and the plugin diagnostics are then built from that single walk's results.
2785
2966
  def rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
2786
2967
  root = parse_result.value
2787
- node_collectors = CheckRules.build_node_collectors(path, index, root)
2968
+ node_collectors = CheckRules.build_node_collectors(
2969
+ path, index, root, plugin_api_surface: CheckRules::PluginApiSurface.active?(@configuration)
2970
+ )
2788
2971
  node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
2789
2972
  diagnostics = CheckRules.diagnose(
2790
2973
  path: path,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+
3
5
  module Rigor
4
6
  module Analysis
5
7
  # ADR-24 slice 4a — records every *implicit-self* method call the inference engine could not resolve to any
@@ -25,8 +27,25 @@ module Rigor
25
27
  # Modelled on {DependencyRecorder}: process-thread-local accumulator, a cheap disabled fast path, and a
26
28
  # frozen snapshot for consumers.
27
29
  module SelfCallResolutionRecorder
28
- KEY = :__rigor_self_call_resolution_recorder__
29
- private_constant :KEY
30
+ # ADR-129 WD1 — the installed accumulator is a Frame; its contents are the bound Table {CALLS}. The activation
31
+ # count below is a process switch, outside the typing state (WD5).
32
+ RECORDER = TypingState.frame(
33
+ :self_call_resolution_recorder,
34
+ contents: :table, reason: "the accumulator a recording installs; each unresolved call records into it",
35
+ # G1′ witness: the memo journals and replays the records, so it is not a shut switch (unlike the tracers).
36
+ witness: "spec/fixtures/typing_state_witnesses/self_call_resolution_recorder.rb"
37
+ )
38
+ private_constant :RECORDER
39
+
40
+ # ADR-129 WD1 — the recorded misses are a bound Table: each installed accumulator is one of its stores, keyed by
41
+ # the call tuple (deduplicated, so the first record of a tuple is its only write), journaled while a capture is
42
+ # open so a later `type_of` memo can validate the reads and replay the writes in order.
43
+ CALLS = TypingState.table(
44
+ :self_call_misses,
45
+ bound: true,
46
+ reason: "UnresolvedSelfCall => true per consumer accumulator, appended in first-record order"
47
+ )
48
+ private_constant :CALLS
30
49
 
31
50
  # One unresolved implicit-self call. `class_name` is the receiver's statically known class (the
32
51
  # enclosing `self` type); `method_name` the called name; `node` the Prism `CallNode` (held in-memory so
@@ -45,10 +64,28 @@ module Rigor
45
64
  @seen = Set.new
46
65
  end
47
66
 
48
- def add(call)
49
- return unless @seen.add?(call)
67
+ def add?(call)
68
+ return false unless @seen.add?(call)
50
69
 
51
70
  @calls << call
71
+ true
72
+ end
73
+
74
+ # Whether `call` is already recorded.
75
+ def recorded?(call) = @seen.include?(call)
76
+
77
+ # The bound Table's store protocol ({CALLS}): `true` for a recorded call, else `default`.
78
+ def fetch(call, default) = @seen.include?(call) || default
79
+
80
+ # The bound Table's store protocol: an undone write (G1's stage-4 form) forgets `call`.
81
+ def delete(call)
82
+ @calls.delete(call) if @seen.delete?(call)
83
+ end
84
+
85
+ # The bound Table's store protocol: a replayed write records `call` as {#add?} does, so the replay order of a
86
+ # write slice is the order of {#calls}.
87
+ def []=(call, _value)
88
+ add?(call)
52
89
  end
53
90
 
54
91
  def snapshot
@@ -69,15 +106,16 @@ module Rigor
69
106
  # Activates recording for `consumer` (the path being analyzed) for the duration of the block and returns
70
107
  # the frozen {Record}. Nests safely; restores the previous recorder on exit.
71
108
  def record_for(consumer)
72
- previous = Thread.current[KEY]
73
109
  accumulator = Accumulator.new(consumer.to_s)
74
- Thread.current[KEY] = accumulator
75
- @mutex.synchronize { @active_count += 1 }
76
- yield
77
- accumulator.snapshot
78
- ensure
79
- Thread.current[KEY] = previous
80
- @mutex.synchronize { @active_count -= 1 }
110
+ TypingState.current.with(RECORDER, accumulator) do
111
+ @mutex.synchronize { @active_count += 1 }
112
+ begin
113
+ yield
114
+ accumulator.snapshot
115
+ ensure
116
+ @mutex.synchronize { @active_count -= 1 }
117
+ end
118
+ end
81
119
  end
82
120
 
83
121
  # Plain integer read (GVL-atomic) — no `Thread.current` lookup on the disabled fast path.
@@ -88,19 +126,20 @@ module Rigor
88
126
  # Records one unresolved implicit-self call. No-op when no consumer is active on this thread (another
89
127
  # thread may have flipped {active?}).
90
128
  def record(class_name:, method_name:, node:, path:, line:, column:)
91
- accumulator = Thread.current[KEY]
129
+ state = TypingState.current
130
+ accumulator = state[RECORDER]
92
131
  return if accumulator.nil?
93
132
 
94
- accumulator.add(
95
- UnresolvedSelfCall.new(
96
- class_name: class_name.to_s,
97
- method_name: method_name.to_sym,
98
- node: node,
99
- path: path,
100
- line: line,
101
- column: column
102
- )
133
+ call = UnresolvedSelfCall.new(
134
+ class_name: class_name.to_s,
135
+ method_name: method_name.to_sym,
136
+ node: node,
137
+ path: path,
138
+ line: line,
139
+ column: column
103
140
  )
141
+ state.log_bound_read(CALLS, accumulator, call, accumulator.fetch(call, TypingState::ABSENT)) if state.capturing?
142
+ state.log_bound_write(CALLS, accumulator, call, true, TypingState::ABSENT) if accumulator.add?(call)
104
143
  end
105
144
  end
106
145
  end