rigortype 0.4.6 → 0.4.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 (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -10,6 +10,7 @@ require_relative "../source/constant_path"
10
10
  require_relative "../source/node_children"
11
11
  require_relative "../source/node_walker"
12
12
  require_relative "../analysis/self_call_resolution_recorder"
13
+ require_relative "../run_scoped_slots"
13
14
  require_relative "block_call_timing"
14
15
  require_relative "block_repetition"
15
16
  require_relative "project_yielder"
@@ -345,7 +346,7 @@ module Rigor
345
346
  self.class.recorded_inject_accumulator(block, scope)
346
347
  end
347
348
 
348
- INJECT_ACCUMULATOR_KEY = :__rigor_inject_accumulator__
349
+ INJECT_ACCUMULATOR_KEY = RunScopedSlots.register(:__rigor_inject_accumulator__)
349
350
  private_constant :INJECT_ACCUMULATOR_KEY
350
351
 
351
352
  # The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
@@ -360,13 +361,18 @@ module Rigor
360
361
  # recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
361
362
  # empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives on
362
363
  # `Thread.current`, independent of any one `ExpressionTyper` instance.
363
- def self.harvest_return_memo
364
- slot = Thread.current[RETURN_MEMO_KEY]
365
- return {} if slot.nil?
364
+ #
365
+ # `bucket` defaults to the calling thread's current one; `Runner#run` keeps the bucket its run filled
366
+ # ({.current_return_memo}) and passes it, since the run releases the thread's slot when it ends.
367
+ def self.harvest_return_memo(bucket = current_return_memo)
368
+ return {} if bucket.nil?
366
369
 
367
- slot[1].transform_values(&:values)
370
+ bucket.transform_values(&:values)
368
371
  end
369
372
 
373
+ # The calling thread's return memo bucket (`{ def_node => { descriptor => MemoEntry } }`), or nil.
374
+ def self.current_return_memo = Thread.current[RETURN_MEMO_KEY]&.last
375
+
370
376
  # Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
371
377
  # `nil` outside such a frame. Read by {#type_of_yield}.
372
378
  #
@@ -1100,7 +1106,6 @@ module Rigor
1100
1106
  conditions = when_node.respond_to?(:conditions) ? when_node.conditions : []
1101
1107
  body_scope, next_scope = Narrowing.case_when_scopes(node.predicate, conditions, clause_scope)
1102
1108
  certainty = case_when_branch_certainty(subject_type, when_node)
1103
- # :no — drop the branch
1104
1109
  candidates << case_arm_type(when_node, body_scope) unless certainty == :no
1105
1110
  if certainty == :yes
1106
1111
  reached_yes = true
@@ -1519,9 +1524,8 @@ module Rigor
1519
1524
 
1520
1525
  # Stable single-hop chain narrowing — `receiver.method` after an `is_a?` / `kind_of?` / `instance_of?`
1521
1526
  # predicate established the narrowing on the dominated edge. The call MUST be no-arg + no-block +
1522
- # rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. ROADMAP
1523
- # § Future cycles — "Method-call receiver narrowing across stable receivers" — Law-of-Demeter-justified
1524
- # single-hop scope.
1527
+ # rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. The
1528
+ # single hop is Law-of-Demeter-justified.
1525
1529
  def method_chain_narrowing_for(node)
1526
1530
  return nil unless node.is_a?(Prism::CallNode)
1527
1531
  return nil unless node.block.nil?
@@ -1771,7 +1775,23 @@ module Rigor
1771
1775
  def call_type_for(node)
1772
1776
  return safe_navigation_call_type(node) if node.safe_navigation?
1773
1777
 
1774
- attribute_write_value(node, call_dispatch_type_for(node))
1778
+ memo_object_value(node, attribute_write_value(node, call_dispatch_type_for(node)))
1779
+ end
1780
+
1781
+ # Issue #2411 — `recv.each_with_object(memo) { |x, acc| acc[k] = v }` returns the memo as the block's
1782
+ # stores leave it. The statement evaluator owns that join ({StatementEvaluator#each_with_object_value}),
1783
+ # and reading it here gives every value position the statement's answer: a method's tail is typed through
1784
+ # this typer, and read off the dispatch alone it was the memo argument's literal `{}` / `[]`. A `break` ends
1785
+ # the call with its own value, so the block's break arms ({#call_break_arm_types}) join the memo: the join
1786
+ # alone read `break :stop if x == 2; a << x` as `Array[1 | 2]`, where Ruby answers `:stop`.
1787
+ def memo_object_value(node, result)
1788
+ return result unless node.name == :each_with_object && node.block.is_a?(Prism::BlockNode)
1789
+
1790
+ memo = StatementEvaluator.new(scope: scope, tracer: tracer).each_with_object_value(node, result)
1791
+ return memo if memo.equal?(result)
1792
+
1793
+ breaks = call_break_arm_types(node)
1794
+ breaks.empty? ? memo : Type::Combinator.union(memo, *breaks)
1775
1795
  end
1776
1796
 
1777
1797
  # The RHS-value override for plain attribute / index writes (#520); a non-write call keeps the
@@ -2388,14 +2408,12 @@ module Rigor
2388
2408
  #
2389
2409
  # - the receiver is not `Nominal[T]` for some T;
2390
2410
  # - no def_node is recorded for that class/method (the receiver is foreign or has only an RBS sig);
2391
- # - the def has no body, or has a parameter shape we cannot bind from the call's positional args;
2392
- # - the inference is already in progress for this (class, method, signature) tuple — recursion safety
2393
- # net.
2411
+ # - the def has no body, or has a parameter shape we cannot bind from the call's positional args.
2394
2412
  # v0.0.3 A — re-types a top-level (or DSL-block-nested) `def` discovered by `ScopeIndexer` under the
2395
2413
  # `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the `infer_user_method_return` shape but uses the current
2396
2414
  # `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
2397
2415
  # consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
2398
- # body is empty, or when a recursion cycle is detected.
2416
+ # body is empty, or when the inference raises.
2399
2417
  def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
2400
2418
  infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
2401
2419
  rescue StandardError
@@ -2442,9 +2460,9 @@ module Rigor
2442
2460
 
2443
2461
  # True when `type` is a concrete value — a `Type::Constant` or a `Type::Tuple` whose elements are
2444
2462
  # (recursively) all value-pinned. ADR-55 slice 1: a value-pinned self-call result is adopted even
2445
- # inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A
2446
- # concrete value at a call site is strictly more precise and can never enable an undefined-method or
2447
- # argument-type false positive — it is FP-neutral by construction.
2463
+ # inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A concrete
2464
+ # value at a call site is strictly more precise and can never enable an undefined-method or argument-type
2465
+ # false positive — it is FP-neutral by construction.
2448
2466
  def fully_value_pinned?(type)
2449
2467
  case type
2450
2468
  when Type::Constant then true
@@ -2646,9 +2664,63 @@ module Rigor
2646
2664
  found = scope.singleton_def_through_ancestors(class_name, method_name)
2647
2665
  owner = found.last
2648
2666
  found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
2667
+ found = [nil, nil] if found.last && dynamic_singleton_definer_ahead?(class_name, method_name, found.last)
2649
2668
  table[key] = found
2650
2669
  end
2651
2670
 
2671
+ SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
2672
+ private_constant :SINGLETON_WILDCARD_SLOT
2673
+
2674
+ # Issue #2284 — a project entry nearer than the `def`'s owner on the singleton chain may define the name
2675
+ # without a `def`, which the def tables do not hold: `class S < B; define_singleton_method(:x) { "s" }; end`
2676
+ # calls S's block, not `B.x`. The farther `def` is then withheld and the call types `Dynamic`. A class's own
2677
+ # singleton side is read from the visibility-contest census: `define_singleton_method(:x)`,
2678
+ # `singleton_class.define_method(:x)` / `attr_*` and a computed name contest the entry's slot whether or not a
2679
+ # `def` is recorded (#1984, #2013). An `extend`ed module's side is read from `discovered_methods`, which holds
2680
+ # its `attr_*` and `define_method` rows, and the module's `def self.extended` hook may define any name on the
2681
+ # extender, so it withholds too.
2682
+ #
2683
+ # An `inherited` hook anywhere on the chain past the receiver's own entry runs when the receiver's class is
2684
+ # created and may define any singleton method on it (`def self.inherited(s) = s.define_singleton_method(:x)`),
2685
+ # so a `def` the receiver only inherits is withheld then as well: a class's `def self.inherited`, or an
2686
+ # `inherited` an extended module defines.
2687
+ def dynamic_singleton_definer_ahead?(class_name, method_name, owner)
2688
+ name = method_name.to_sym
2689
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
2690
+ return true if class_name.to_s != owner.to_s && inherited_hook_on_chain?(chain, class_name.to_s)
2691
+
2692
+ chain.entries.each do |entry|
2693
+ next if entry.external?
2694
+ return false if entry.name == owner.to_s
2695
+ return true if entry_defines_without_def?(entry, name)
2696
+ end
2697
+ false
2698
+ end
2699
+
2700
+ def inherited_hook_on_chain?(chain, receiver_name)
2701
+ chain.entries.any? do |entry|
2702
+ next false if entry.external? || entry.name == receiver_name
2703
+
2704
+ if entry.side == :singleton
2705
+ !scope.singleton_def_for(entry.name, :inherited).nil?
2706
+ else
2707
+ scope.discovered_method?(entry.name, :inherited, :instance)
2708
+ end
2709
+ end
2710
+ end
2711
+
2712
+ def entry_defines_without_def?(entry, name)
2713
+ unless entry.side == :singleton
2714
+ return scope.discovered_method?(entry.name, name, :instance) ||
2715
+ !scope.singleton_def_for(entry.name, :extended).nil?
2716
+ end
2717
+
2718
+ discovery = scope.discovery
2719
+ # No edge of its own: `singleton_def_through_ancestors`' chain search filed every entry ahead of the owner.
2720
+ discovery.contested?(:discovered_method_visibilities, [entry.name, [:singleton, name]]) ||
2721
+ discovery.contested?(:discovered_method_visibilities, [entry.name, SINGLETON_WILDCARD_SLOT])
2722
+ end
2723
+
2652
2724
  # The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
2653
2725
  # body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
2654
2726
  # `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
@@ -2684,7 +2756,7 @@ module Rigor
2684
2756
  # external entries this lookup passes over. Cross-file: the chain is built from the tables the runner
2685
2757
  # seeds from the project-wide pre-pass, cycle-guarded and capped at `Scope::ANCESTOR_WALK_LIMIT`.
2686
2758
 
2687
- CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
2759
+ CLASS_GRAPH_CACHE_KEY = RunScopedSlots.register(:__rigor_class_graph_cache__)
2688
2760
  private_constant :CLASS_GRAPH_CACHE_KEY
2689
2761
 
2690
2762
  # Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
@@ -2885,7 +2957,7 @@ module Rigor
2885
2957
  dynamic_top
2886
2958
  end
2887
2959
 
2888
- OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
2960
+ OVERRIDE_GATE_CACHE_KEY = RunScopedSlots.register(:__rigor_overridable_method_gate__)
2889
2961
  private_constant :OVERRIDE_GATE_CACHE_KEY
2890
2962
 
2891
2963
  # Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
@@ -2958,7 +3030,7 @@ module Rigor
2958
3030
  EMPTY_REDEFINERS = [].freeze
2959
3031
  private_constant :EMPTY_REDEFINERS
2960
3032
 
2961
- METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
3033
+ METHOD_DEFINERS_INDEX_KEY = RunScopedSlots.register(:__rigor_method_definers_index__)
2962
3034
  private_constant :METHOD_DEFINERS_INDEX_KEY
2963
3035
 
2964
3036
  # The second way. Two flat thread slots rather than one slot holding an array of ways: a
@@ -2966,7 +3038,7 @@ module Rigor
2966
3038
  # elements are pinned, and the engine then reads the `way[0].equal?(…)` guards below as always-falsey
2967
3039
  # (`make check` fires `flow.always-truthy-condition` on this very file). Two keys read as two plain
2968
3040
  # `Thread.current` reads and keep the guards analysable.
2969
- METHOD_DEFINERS_INDEX_ALT_KEY = :__rigor_method_definers_index_alt__
3041
+ METHOD_DEFINERS_INDEX_ALT_KEY = RunScopedSlots.register(:__rigor_method_definers_index_alt__)
2970
3042
  private_constant :METHOD_DEFINERS_INDEX_ALT_KEY
2971
3043
 
2972
3044
  # `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
@@ -3000,8 +3072,9 @@ module Rigor
3000
3072
  # the singleton index.
3001
3073
  #
3002
3074
  # DELIBERATELY not keyed on the whole {Scope::DiscoveryIndex}, unlike {#class_graph_buckets} and
3003
- # {#override_gate_buckets} above. {#build_method_definers_index} reads the ONE table it is handed and
3004
- # nothing else, so the def tables are already the complete key, and the index object is a strictly
3075
+ # {#override_gate_buckets} above. {DefinersIndex.for} reads the ONE table it is handed, and the seed that
3076
+ # table was merged over, and nothing else, so the def tables are already the complete key, and the index
3077
+ # object is a strictly
3005
3078
  # narrower one: the project-seed scope carries a fresh index per file while its def tables stay the
3006
3079
  # same object. Over the 200 synthetic files above, an index-keyed memo misses all 400 times where a
3007
3080
  # def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
@@ -3019,17 +3092,7 @@ module Rigor
3019
3092
  Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
3020
3093
  end
3021
3094
  singleton = kind == :singleton
3022
- way[singleton ? 3 : 2] ||= build_method_definers_index(singleton ? singleton_def_nodes : def_nodes)
3023
- end
3024
-
3025
- def build_method_definers_index(table)
3026
- index = {}
3027
- table.each do |class_name, methods|
3028
- next if class_name == Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
3029
-
3030
- methods.each_key { |method_name| (index[method_name] ||= []) << class_name }
3031
- end
3032
- index
3095
+ way[singleton ? 3 : 2] ||= DefinersIndex.for(singleton ? singleton_def_nodes : def_nodes)
3033
3096
  end
3034
3097
 
3035
3098
  # True when `candidate`'s ancestor chain (superclasses + included/prepended modules) reaches `owner` —
@@ -3110,10 +3173,11 @@ module Rigor
3110
3173
  # `[receiver, *args]` descriptor tuple; a callee may appear under at most two node identities per run
3111
3174
  # (the project-index parse every OTHER file resolves through, plus the defining file's own analysis
3112
3175
  # parse). Only the current generation's bucket is retained (single slot) — a rolled-over run's entries
3113
- # become garbage instead of accumulating across LSP runs. `ExpressionTyper` is rebuilt per
3114
- # `Scope#type_of`, so the store lives on `Thread.current`; fork-pool workers are separate processes,
3115
- # so it never crosses a project boundary (ADR-84 WD5).
3116
- RETURN_MEMO_KEY = :__rigor_user_method_return_memo__
3176
+ # become garbage instead of accumulating across LSP runs, and the run that filled the slot releases it
3177
+ # when it ends ({RunScopedSlots}; `Runner#run` keeps its bucket for `#return_summaries`).
3178
+ # `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store lives on `Thread.current`; fork-pool
3179
+ # workers are separate processes, so it never crosses a project boundary (ADR-84 WD5).
3180
+ RETURN_MEMO_KEY = RunScopedSlots.register(:__rigor_user_method_return_memo__)
3117
3181
  private_constant :RETURN_MEMO_KEY
3118
3182
 
3119
3183
  # ADR-84 WD2 — one memo entry: the final inferred return plus, when the entry was computed under
@@ -3898,7 +3962,7 @@ module Rigor
3898
3962
  # Consumes one unit from the thread-local unroll-fuel counter and returns the units that were
3899
3963
  # available *before* this consumption (so a positive return means the extended value-key may be used).
3900
3964
  # Fuel is per-outermost inference entry: at the top level (empty guard stack) it seeds to
3901
- # `RECURSION_UNROLL_FUEL`, and the `ensure` in `infer_user_method_return` clears it once the stack
3965
+ # `RECURSION_UNROLL_FUEL`, and the `ensure` in `evaluate_guarded_user_method_body` clears it once the stack
3902
3966
  # drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
3903
3967
  # keeps the plain `(receiver, method)` signature — today's behaviour.
3904
3968
  def unroll_fuel_remaining(stack)
@@ -3961,7 +4025,7 @@ module Rigor
3961
4025
  # to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
3962
4026
  # `discovered_classes` / `class_ivars` / etc. the caller does), and the call's `arg_types` bound to
3963
4027
  # the parameters. Returns nil when the binding is truly ambiguous (arity mismatch, trailing `post`
3964
- # params, `...` forwarding).
4028
+ # params).
3965
4029
  #
3966
4030
  # Issue #524 — the binder is per-PARAMETER, not per-signature. The first iteration declined any def
3967
4031
  # with an optional / rest / keyword / block parameter outright, so ONE `options = {}` default zeroed
@@ -4009,7 +4073,7 @@ module Rigor
4009
4073
  body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(def_node.body, body_scope))
4010
4074
  end
4011
4075
 
4012
- # Issue #681 / #707 — the ONE place in `lib/` that answers "what `Module.nesting` was recorded for this
4076
+ # Issue #681 / #707 — the typer's answer to "what `Module.nesting` was recorded for this
4013
4077
  # def node". `Scope::DiscoveryIndex#discovered_def_nestings` is the owner; the resolver memo is that
4014
4078
  # table's REHYDRATION for the one population it cannot hold — a def served from an ADR-85 seed bundle,
4015
4079
  # whose node {Inference::DefNodeResolver} mints from its own parse, so no table keyed by the identities
@@ -4024,7 +4088,7 @@ module Rigor
4024
4088
  # run and an `--incremental` recheck answering the same constant for the same body; the rehydration half
4025
4089
  # is populated only inside `DefNodeResolver.with_run`, and outside it this falls back to the peel.
4026
4090
  def recorded_def_nesting(def_node)
4027
- scope.discovery.discovered_def_nestings[def_node] || DefNodeResolver.rehydrated_nesting(def_node)
4091
+ scope.discovery.recorded_def_nesting(def_node)
4028
4092
  end
4029
4093
 
4030
4094
  # The body scope's fold-safe set: the body's own struct locals, plus issue #525's `:self` sentinel
@@ -4353,13 +4417,15 @@ module Rigor
4353
4417
 
4354
4418
  # Whether the call may run its block more than once, so a later run reads a captured binding an earlier
4355
4419
  # run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
4356
- # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). Issue #2301 — a
4357
- # project callee that hands the block on ({ProjectYielder.hands_block_on?}) also counts: the block's value is what
4358
- # the forward's target `yield`s, and that target may run it many times, so a captured local the block rebinds must
4359
- # not read its entry binding.
4420
+ # {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). A project callee
4421
+ # whose body may run the block more than once on one path ({ProjectYielder.block_value_may_repeat?}) also
4422
+ # counts: a `yield` in a loop or a core iterator's block (`xs.each { |x| v = yield(x) }`, issue #2303), or a
4423
+ # hand-off (`g(x, &)`, issue #2301), whose target may run it many times, so a captured local the block rebinds
4424
+ # must not read its entry binding. A callee that runs it once per path (`f ? yield(1) : yield(2)`,
4425
+ # `then { yield }`) keeps the single-run value.
4360
4426
  def block_may_repeat?(call_node, receiver_type)
4361
4427
  BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
4362
- ProjectYielder.hands_block_on?(call_node, receiver_type, scope)
4428
+ ProjectYielder.block_value_may_repeat?(call_node, receiver_type, scope)
4363
4429
  end
4364
4430
 
4365
4431
  EMPTY_BREAK_ARMS = [].freeze
@@ -5569,8 +5635,9 @@ module Rigor
5569
5635
  end
5570
5636
 
5571
5637
  # Records `type` as the accumulator `block` receives when typed from this typer's scope
5572
- # ({.recorded_inject_accumulator}). The keys are weak, so a finished file's nodes do not stay reachable, and
5573
- # the values strong, so a record outlives a collection while its node does. Prism nodes compare by identity.
5638
+ # ({.recorded_inject_accumulator}). Prism nodes compare by identity. The keys are weak but the values strong,
5639
+ # and a recorded scope reaches the parse tree that holds the key, so a record never goes on its own (issue
5640
+ # #2412): the map is a run-scoped slot ({RunScopedSlots}), emptied when the run that filled it ends.
5574
5641
  def record_inject_accumulator(block, type)
5575
5642
  (Thread.current[INJECT_ACCUMULATOR_KEY] ||= ObjectSpace::WeakKeyMap.new)[block] = [scope, type].freeze
5576
5643
  end
@@ -28,13 +28,12 @@ module Rigor
28
28
  # a flag threaded into `Scope`: an ADR-2 plugin-facing bypass of dependency recording would produce
29
29
  # a silently stale warm cache that no diagnostic diff can show.
30
30
  #
31
- # There is deliberately NO memo here. The dispatch consumer this module is being extracted FOR does
32
- # not exist yet, and a memo keyed on the tables this slice happens to name would be keyed on less
31
+ # There is deliberately NO memo here. A memo keyed on the tables the walk names would be keyed on less
33
32
  # than the walk reads — the ancestor chain resolves names through `known_user_class?`
34
33
  # (`discovered_def_nodes`, `discovered_methods`) and `ancestor_name_candidates`
35
34
  # (`discovered_header_nestings`). The chain itself is memoised per discovery index
36
- # ({Scope::ResolutionChain}); the slice that brings the hot path brings this module's memo, keyed on
37
- # what that consumer actually reads.
35
+ # ({Scope::ResolutionChain}); the dispatch callers in {MethodDispatcher::RbsDispatch} memoise their
36
+ # own answers, keyed on what they read.
38
37
  module ExternalAncestorResolution
39
38
  # The owner a name must PRECEDE for its declaration to win an MRO. See {declared_before_object?}.
40
39
  OBJECT_OWNER = "Object"
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../source/constant_path"
6
6
  require_relative "../source/node_children"
7
7
  require_relative "ivar_writer_census"
8
+ require_relative "stored_block_call"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -28,7 +29,15 @@ module Rigor
28
29
  # - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
29
30
  # `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
30
31
  # - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
31
- # `[:ivar_set_class, class_name]` and `[:own_write, class_name, name, path]`.
32
+ # `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]` and, issue #2079,
33
+ # `[:own_write_type, class_name, name, value, path]`.
34
+ # - `[:global_writer, name, method]` — issue #2314: a write of the global `name` inside a method body, keyed by
35
+ # the `def`'s name (or the literal name a `define_method` block defines), or by nil inside a lambda or a block
36
+ # its call keeps to run later ({StoredBlockCall}). A write in a block its call runs at once belongs to the
37
+ # enclosing body. A write at the top level or in a class body records nothing, and neither does a write of
38
+ # the frame-local `$_` or `$~`. {GlobalWriterReach} reads these.
39
+ # - `[:method_alias, new, old]` — issue #2314: `alias new old`, `alias_method :new, :old` or `def_delegator
40
+ # :@o, :old, :new`, whose new name runs a method named old. {GlobalWriterReach} follows it.
32
41
  #
33
42
  # The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
34
43
  # `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
@@ -78,7 +87,7 @@ module Rigor
78
87
  end
79
88
 
80
89
  # #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
81
- IVAR_KINDS = %i[ivar_set ivar_set_class own_write].to_set.freeze
90
+ IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type].to_set.freeze
82
91
 
83
92
  def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
84
93
 
@@ -87,6 +96,7 @@ module Rigor
87
96
  case entry.first
88
97
  when :ivar_set then "ivar_set:#{entry[1]}"
89
98
  when :ivar_set_class then "ivar_set_class:#{entry[1]}"
99
+ when :own_write_type then "own_write_type:#{entry[1]}##{entry[2]}"
90
100
  else "own_write:#{entry[1]}##{entry[2]}"
91
101
  end
92
102
  end
@@ -140,6 +150,7 @@ module Rigor
140
150
  @refine_ranges = []
141
151
  @ivar_writers = IvarWriterCensus.new(@census)
142
152
  @in_effect = in_effect
153
+ @bodies = []
143
154
  end
144
155
 
145
156
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
@@ -148,6 +159,7 @@ module Rigor
148
159
  # the host walk does not track it, or no class owns an instance there).
149
160
  def visit(node, top_level:, owner: nil)
150
161
  @ivar_writers.visit(node, owner)
162
+ visit_global_writer(node)
151
163
  case node
152
164
  when Prism::AliasGlobalVariableNode then visit_global_alias(node)
153
165
  when Prism::DefNode then record(node, node.name)
@@ -160,6 +172,68 @@ module Rigor
160
172
 
161
173
  private
162
174
 
175
+ # Issue #2314 — tracks the method bodies, lambdas and stored blocks of a pre-order descent as flat
176
+ # `start, end, label` triples (`@bodies`, innermost last), and records a global write inside one
177
+ # ({GlobalWriterReach}). A body ending before the write starts is closed. Only those nodes read their location.
178
+ def visit_global_writer(node)
179
+ case node
180
+ when Prism::DefNode then open_body(node, node.name)
181
+ when Prism::LambdaNode then open_body(node, nil)
182
+ when Prism::AliasMethodNode then record_method_alias(node.new_name, node.old_name)
183
+ when Prism::CallNode
184
+ open_stored_block(node)
185
+ visit_alias_call(node)
186
+ when Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
187
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode
188
+ record_global_writer(node)
189
+ end
190
+ end
191
+
192
+ # `$_` and `$~` are the frame's own: a method's write to one never reaches its caller.
193
+ FRAME_LOCAL_GLOBALS = %i[$_ $~].freeze
194
+ private_constant :FRAME_LOCAL_GLOBALS
195
+
196
+ def record_global_writer(node)
197
+ return if FRAME_LOCAL_GLOBALS.include?(node.name)
198
+
199
+ start = node.location.start_offset
200
+ bodies = @bodies
201
+ bodies.pop(3) while !bodies.empty? && bodies[-2] <= start
202
+ index = bodies.size - 3
203
+ index -= 3 while index >= 0 && !(bodies[index] <= start && start < bodies[index + 1])
204
+ @census << [:global_writer, node.name, bodies[index + 2]].freeze if index >= 0
205
+ end
206
+
207
+ def open_body(node, label)
208
+ location = node.location
209
+ @bodies.push(location.start_offset, location.end_offset, label)
210
+ end
211
+
212
+ # `alias_method :new, :old`, and `def_delegator :@o, :old, :new` (and its instance and single spellings), whose
213
+ # new name runs a method of the old name.
214
+ def visit_alias_call(node)
215
+ arguments = node.arguments&.arguments
216
+ case node.name
217
+ when :alias_method then record_method_alias(arguments[0], arguments[1]) if arguments&.size == 2
218
+ when :def_delegator, :def_instance_delegator, :def_single_delegator
219
+ record_method_alias(arguments[2], arguments[1]) if arguments&.size == 3
220
+ end
221
+ end
222
+
223
+ def record_method_alias(new_node, old_node)
224
+ new_name = literal_name(new_node)
225
+ old_name = literal_name(old_node)
226
+ @census << [:method_alias, new_name, old_name].freeze if new_name && old_name && new_name != old_name
227
+ end
228
+
229
+ def open_stored_block(node)
230
+ block = node.block
231
+ return unless block.is_a?(Prism::BlockNode) && StoredBlockCall.stores_block?(node)
232
+
233
+ definer = %i[define_method define_singleton_method].include?(node.name)
234
+ open_body(block, definer ? literal_name(node.arguments&.arguments&.first) : nil)
235
+ end
236
+
163
237
  def visit_global_alias(node)
164
238
  [node.new_name, node.old_name].each do |side|
165
239
  @census << GlobalWriteCensus.alias_entry(side.name) if side.is_a?(Prism::GlobalVariableReadNode)