rigortype 0.4.5 → 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 (452) 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 +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -524,7 +524,7 @@ module Rigor
524
524
 
525
525
  # Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
526
526
  # does an instance variable (#1446).
527
- slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
527
+ slot = case_subject_slot(subject, scope)
528
528
  return [body_scope, scope] if slot.nil?
529
529
 
530
530
  truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
@@ -538,6 +538,32 @@ module Rigor
538
538
  CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
539
539
  private_constant :CASE_SUBJECT_KINDS
540
540
 
541
+ # The slot a `case` subject narrows. Issue #2211 — a subject that writes a local or instance variable
542
+ # (`case (x = v)`, `case x = v`) tests the value it stores, which the variable holds in every clause, so the
543
+ # clauses narrow the variable as they narrow a bare read of it: `case (x = s ? String(s) : nil) when String then
544
+ # x.upcase end` reads `x` as `String`. A single-statement `(…)` is unwrapped.
545
+ def case_subject_slot(subject, scope)
546
+ subject = unwrap_case_subject(subject)
547
+ case subject
548
+ when Prism::LocalVariableWriteNode
549
+ variable_slot(:local, subject.name, scope.local(subject.name), CASE_SUBJECT_KINDS)
550
+ when Prism::InstanceVariableWriteNode
551
+ variable_slot(:ivar, subject.name, scope.ivar(subject.name), CASE_SUBJECT_KINDS)
552
+ else receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
553
+ end
554
+ end
555
+ private_class_method :case_subject_slot
556
+
557
+ # `subject` with each single-statement `(…)` around it removed: the node {.case_subject_slot} narrows through,
558
+ # which `StatementEvaluator#when_test_rebinds_subject?` reads the same way.
559
+ def unwrap_case_subject(subject)
560
+ while subject.is_a?(Prism::ParenthesesNode) && subject.body.is_a?(Prism::StatementsNode) &&
561
+ subject.body.body.size == 1
562
+ subject = subject.body.body.first
563
+ end
564
+ subject
565
+ end
566
+
541
567
  # Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
542
568
  # through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
543
569
  # which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
@@ -582,6 +608,26 @@ module Rigor
582
608
  truthy
583
609
  end
584
610
 
611
+ # Issue #2443 — `scope` with what every match binds, laid over the bindings it holds, for the block of a call
612
+ # that yields only after it has matched in the caller's frame ({MatchRebinding::YieldingMatch}): `$~` a
613
+ # `MatchData`, and `$&` and the pre- and post-match a `String`. The numbered groups and `$+` keep the bindings
614
+ # `scope` gives them. Typing them from the call's own pattern needs the group walk
615
+ # ({#unconditional_capture_groups}) to read every source right, which it does not yet (#2472), and forgetting
616
+ # them would report a group an earlier match in the method narrowed.
617
+ def yielded_match_scope(scope)
618
+ string_t = Type::Combinator.nominal_of("String")
619
+ scope.with_global(:$~, Type::Combinator.nominal_of("MatchData"))
620
+ .with_global(:$&, string_t).with_global(:$`, string_t).with_global(:$', string_t)
621
+ end
622
+
623
+ # Issue #2443 — true when every member of `type` is a String (a project subclass included) or a `Dynamic` that
624
+ # is untyped or may hold only Strings, Symbols and nil ({#match_member_kind}). A `nil` member never runs the
625
+ # block, so it counts neither way.
626
+ def string_match_receiver?(type, scope)
627
+ kinds = match_member_kinds(type, scope) - [:nil]
628
+ !kinds.empty? && kinds.all? { |kind| %i[string string_subclass dynamic].include?(kind) }
629
+ end
630
+
585
631
  # Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
586
632
  # recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
587
633
  # entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
@@ -942,12 +988,22 @@ module Rigor
942
988
  end
943
989
 
944
990
  def analyse_local_read(node, scope)
945
- current = scope.local(node.name)
991
+ local_truthiness_edges(node.name, scope)
992
+ end
993
+
994
+ # The falsey edge is where an optimistic lookup's miss lands (ADR-101, issue #2306): `Scope#with_local`
995
+ # drops the issue #286 mark as a write does, so the edge re-takes the one `scope` holds. Without it the
996
+ # join after `if v; …; end` with no `else` lost the mark on both arms and read `v` as nil-free, although
997
+ # a miss reaches that join through the implicit else. The truthy edge proves the value non-nil, so it
998
+ # keeps dropping the mark.
999
+ def local_truthiness_edges(name, scope)
1000
+ current = scope.local(name)
946
1001
  return nil if current.nil?
947
1002
 
1003
+ falsey = scope.with_local(name, narrow_falsey(current))
948
1004
  [
949
- scope.with_local(node.name, narrow_truthy(current)),
950
- scope.with_local(node.name, narrow_falsey(current))
1005
+ scope.with_local(name, narrow_truthy(current)),
1006
+ OptimisticOrigin.carry_local_mark(scope, falsey, name, nil)
951
1007
  ]
952
1008
  end
953
1009
 
@@ -961,13 +1017,7 @@ module Rigor
961
1017
  # holds the just-bound value; the narrowing contract on the surrounding `if` is the
962
1018
  # same.
963
1019
  def analyse_local_write(node, scope)
964
- current = scope.local(node.name)
965
- return nil if current.nil?
966
-
967
- [
968
- scope.with_local(node.name, narrow_truthy(current)),
969
- scope.with_local(node.name, narrow_falsey(current))
970
- ]
1020
+ local_truthiness_edges(node.name, scope)
971
1021
  end
972
1022
 
973
1023
  # Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
@@ -981,9 +1031,11 @@ module Rigor
981
1031
  current = scope.ivar(node.name)
982
1032
  return nil if current.nil?
983
1033
 
1034
+ # The falsey edge keeps the optimistic mark, as {#local_truthiness_edges} explains.
1035
+ falsey = scope.with_ivar(node.name, narrow_falsey(current))
984
1036
  [
985
1037
  scope.with_ivar(node.name, narrow_truthy(current)),
986
- scope.with_ivar(node.name, narrow_falsey(current))
1038
+ OptimisticOrigin.carry_ivar_mark(scope, falsey, node.name, nil)
987
1039
  ]
988
1040
  end
989
1041
 
@@ -1154,15 +1206,6 @@ module Rigor
1154
1206
  refinements
1155
1207
  end
1156
1208
 
1157
- # Recognised CallNode predicates:
1158
- # - `recv.nil?` (Slice 6 phase 1, no args, no block)
1159
- # - unary `!recv` (`name == :!`, no args, no block)
1160
- # - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
1161
- # static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
1162
- # - `local == literal` / `literal == local` and the `!=` mirror for trusted static
1163
- # literals (Slice 6 phase 2 sub-phase 2).
1164
- # Anything else returns nil so the surrounding analyser falls through to the
1165
- # no-narrowing fallback.
1166
1209
  # Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
1167
1210
  # it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
1168
1211
  # variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
@@ -2440,10 +2483,10 @@ module Rigor
2440
2483
  narrowed == original ? :relational : :local_binding
2441
2484
  end
2442
2485
 
2443
- # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
2444
- # MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
2486
+ # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The argument
2445
2487
  # MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
2446
- # qualified class name. Anything else falls through to "no narrowing".
2488
+ # qualified class name. The receiver is a local, a global, constant or instance variable
2489
+ # ({#receiver_slot}) or a single-hop chain; anything else falls through to "no narrowing".
2447
2490
  def analyse_class_predicate(node, scope, exact:)
2448
2491
  return nil if node.arguments.nil?
2449
2492
  return nil unless node.arguments.arguments.size == 1
@@ -2512,14 +2555,13 @@ module Rigor
2512
2555
  class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
2513
2556
  end
2514
2557
 
2515
- # Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
2516
- # receiver narrowing across stable receivers"). When the predicate's receiver is
2558
+ # Stable single-hop method-chain narrowing. When the predicate's receiver is
2517
2559
  # `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
2518
2560
  # narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
2519
2561
  # dominated body's identical chain reads then observe the narrowed type through
2520
2562
  # `ExpressionTyper#call_type_for`'s lookup.
2521
2563
  #
2522
- # Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
2564
+ # Heuristic-by-design: a second call to `x.last` could in
2523
2565
  # principle return a different value than the first. The chain is dropped on (1)
2524
2566
  # receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
2525
2567
  # any intervening call against the same root receiver (handled by
@@ -2762,10 +2804,14 @@ module Rigor
2762
2804
 
2763
2805
  # Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
2764
2806
  # matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
2765
- # anything else. Each member narrows to its String and Symbol parts: a `String | nil` subject reads `String`, a
2766
- # Symbol one stays `Symbol`. The `to_str` conversion is not modelled, so a member that defines `to_str` and is
2767
- # neither drops out of the edge. A `Dynamic` member is left as it is, since it may be a Symbol at run time. The
2768
- # no-match edge keeps the entry type: a String that does not match reaches it too.
2807
+ # anything else. A member is kept whole when its class defines `to_str` (#2002) — a String and its
2808
+ # subclasses, a class whose signature, ancestry or project source declares it — or when no signature
2809
+ # declares the class, and a `Dynamic` member is kept, since it may also be a Symbol at run time. Any other
2810
+ # member narrows to its String and Symbol parts, which are only what can match: a `String | nil` subject
2811
+ # reads `String`, a Symbol one stays `Symbol`, an Integer one drops out. An `Object`, `BasicObject` or
2812
+ # module member narrows too (to `String | Symbol`): its instances may be anything, and reading the edge as
2813
+ # the String it most often is keeps `s.length` on it from reporting. The no-match edge keeps the entry type:
2814
+ # a String that does not match reaches it too.
2769
2815
  def regexp_case_equality_type(current, scope)
2770
2816
  members = current.is_a?(Type::Union) ? current.members : [current]
2771
2817
  Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
@@ -2773,6 +2819,7 @@ module Rigor
2773
2819
 
2774
2820
  def regexp_case_equality_member(member, scope)
2775
2821
  return member if member.is_a?(Type::Dynamic)
2822
+ return member if converts_with_to_str?(member, scope)
2776
2823
 
2777
2824
  parts = %w[String Symbol].map do |class_name|
2778
2825
  narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
@@ -2780,6 +2827,36 @@ module Rigor
2780
2827
  Type::Combinator.union(*parts)
2781
2828
  end
2782
2829
 
2830
+ # True when a class `member` dispatches through declares `to_str`, or is declared by no signature
2831
+ # ({#class_lacks_method?} answers false for it). `Object`, `BasicObject` and a module String or Symbol
2832
+ # includes (`Kernel`, `Comparable`) are read as their String and Symbol parts instead: their instances may be
2833
+ # anything, and the String they most often are keeps a String method on the arm from reporting. Any other
2834
+ # module is kept whole, since a matching value may include it; so is one of those that declares `to_str`.
2835
+ def converts_with_to_str?(member, scope)
2836
+ targets = ProjectMethodOwnership.targets(member)
2837
+ return false if targets.nil? || targets.empty?
2838
+
2839
+ targets.any? { |class_name, kind| class_converts_with_to_str?(class_name, kind, scope) }
2840
+ end
2841
+
2842
+ # The modules String's or Symbol's own ancestry includes, read from the running Ruby.
2843
+ STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
2844
+ private_constant :STRING_SYMBOL_MODULES
2845
+
2846
+ def class_converts_with_to_str?(class_name, kind, scope)
2847
+ return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
2848
+ if kind == :instance && scope.environment.rbs_module?(class_name)
2849
+ return module_declares_to_str?(class_name, scope) || !STRING_SYMBOL_MODULES.include?(class_name)
2850
+ end
2851
+
2852
+ !class_lacks_method?(class_name, :to_str, kind, scope)
2853
+ end
2854
+
2855
+ def module_declares_to_str?(module_name, scope)
2856
+ ProjectMethodOwnership.defines?(module_name, :to_str, :instance, scope) ||
2857
+ !ExternalAncestorResolution.method_definition(module_name, :to_str, :instance, scope: scope).nil?
2858
+ end
2859
+
2783
2860
  def unwrap_parens(node)
2784
2861
  while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
2785
2862
  node.body.body.size == 1
@@ -2803,9 +2880,7 @@ module Rigor
2803
2880
  # - For each fact, the corresponding positional argument (matched by parameter name in
2804
2881
  # the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
2805
2882
  # apply.
2806
- # - When the target is `self`, narrowing applies to the receiver — but the engine does
2807
- # not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
2808
- # but currently produce no scope edits.
2883
+ # - When the target is `self`, narrowing applies to the receiver ({#apply_self_fact}).
2809
2884
  def analyse_rbs_extended_contribution(node, scope)
2810
2885
  method_def = resolve_rbs_extended_method(node, scope)
2811
2886
  return nil if method_def.nil?
@@ -3342,8 +3417,8 @@ module Rigor
3342
3417
  # extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
3343
3418
  # written in a file outside the analysed set are both invisible to the walk behind
3344
3419
  # {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
3345
- # complete — RBS says nothing about a class it does not declare. A shape that turned this table into a positive
3346
- # `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3420
+ # complete — RBS says nothing about a class it does not declare. A shape that turned this table
3421
+ # into a positive `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3347
3422
  # `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
3348
3423
  # it, trading the unreachable-clause false positive for an undefined-method one — the same trap
3349
3424
  # #657 recorded on the `Constant` carrier.
@@ -3525,7 +3600,7 @@ module Rigor
3525
3600
  rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
3526
3601
  end
3527
3602
 
3528
- # The four receiver shapes with a scope binding the edges can rebind, mirroring
3603
+ # The three receiver shapes with a scope binding the edges can rebind, mirroring
3529
3604
  # `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
3530
3605
  # the caller declines.
3531
3606
  def receiver_binding_type(receiver, scope)
@@ -3635,7 +3710,7 @@ module Rigor
3635
3710
 
3636
3711
  # Walks a call chain down to its `&.` link, collecting the suffix method names applied
3637
3712
  # above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
3638
- # direct local / ivar read — those are the two bindings a scope can rewrite — and unless
3713
+ # direct local, instance-variable, global or constant read — the slots a scope can rewrite — and unless
3639
3714
  # every suffix is a bare reader or predicate, since an argument or a block could make the
3640
3715
  # answer depend on something other than the receiver.
3641
3716
  #
@@ -123,9 +123,14 @@ module Rigor
123
123
  # all.
124
124
  def answer_for_entry(scope, entry, method_name)
125
125
  unless entry.external?
126
- return source_defines?(scope, entry.name, method_name) ? [:source, entry.name] : nil
126
+ defines = source_defines?(scope, entry.name, method_name) || macro_named?(scope, entry.name, method_name)
127
+ return defines ? [:source, entry.name] : nil
127
128
  end
128
129
 
130
+ # Issue #2094 — a project module with no `def` is held as an external; a macro of its body may name it.
131
+ named = entry.candidates.find { |candidate| macro_named?(scope, candidate, method_name) }
132
+ return [:source, named] if named
133
+
129
134
  entry.candidates.each do |candidate|
130
135
  definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
131
136
  return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
@@ -137,6 +142,13 @@ module Rigor
137
142
  end
138
143
  private_class_method :answer_for_entry
139
144
 
145
+ # Issue #2094 — whether a class-body macro of `owner` was handed `method_name` as a literal
146
+ # (`dsl_method :xdyn`, `def_delegators :@target, :xdyn`). The indexer files every such name as an opaque
147
+ # envelope, since what the macro defines under it is unknown, so the module may define the name.
148
+ def macro_named?(scope, owner, method_name)
149
+ scope.parameter_envelopes_of(owner).key?([:instance, method_name.to_sym])
150
+ end
151
+
140
152
  def private_definition?(definition)
141
153
  definition.respond_to?(:accessibility) && definition.accessibility == :private
142
154
  end
@@ -18,7 +18,7 @@ module Rigor
18
18
  # that happens to have been produced optimistically.
19
19
  #
20
20
  # Issue #286: the `if` / `unless` branch elision is one consumer of `Narrowing.predicate_certainty`; the
21
- # other two are `flow.always-truthy-condition` and the `&&` / `||` `constant_value_polarity` gate, and the
21
+ # other two are `flow.always-truthy-condition` and `StatementEvaluator#right_operand_dead?`, and the
22
22
  # spec passage above binds all three. This channel is what lets a certainty judgment tell the two apart.
23
23
  #
24
24
  # Issue #313: the mark is attached to a *value* — the call node that produced it, or the local / ivar it
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "mutation_widening"
5
+ require_relative "receiver_alias"
6
+ require_relative "../type"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2499 rule 3 (part of #2494) — the stdlib calls that fill a String the caller passes in. After
11
+ # `io.read(n, buf)`, `io.readpartial(n, buf)`, `io.sysread(n, buf)` and `io.read_nonblock(n, buf)` the buffer
12
+ # holds what was read, and after `StringIO.new(s)` every later write through the StringIO lands in `s`. The
13
+ # argument (a local, instance, class or global variable read) is widened as an in-place `replace` of it would
14
+ # widen it, so a `buf = +""` seed stops reading `""` after the call.
15
+ #
16
+ # A built-in catalogue keyed by method name and argument position, not a receiver-typed dispatch: a method of
17
+ # another class with the same name and a second positional argument loses only precision on that argument.
18
+ module OutBufferMutation
19
+ # Method name => the zero-based position of the buffer argument.
20
+ READERS = { read: 1, readpartial: 1, sysread: 1, read_nonblock: 1 }.freeze
21
+ private_constant :READERS
22
+
23
+ module_function
24
+
25
+ # `scope` with the buffer argument of `call_node` widened, or `scope` itself when the call is no such call or the
26
+ # argument holds no String: `Rails.cache.read(key, opts)` passes an options Hash that keeps its shape.
27
+ def floor(call_node, scope)
28
+ buffer = buffer_argument(call_node)
29
+ return scope if buffer.nil?
30
+
31
+ getter, = MutationWidening::ALIAS_ACCESSORS[buffer.class]
32
+ return scope unless string_carrier?(scope.public_send(getter, ReceiverAlias.read_name(buffer)))
33
+
34
+ MutationWidening.widen_alias_read(:replace, buffer, scope)
35
+ end
36
+
37
+ # A `Constant<String>` or the `String` nominal, or a union of nothing else.
38
+ def string_carrier?(type)
39
+ case type
40
+ when Type::Constant then type.value.is_a?(String)
41
+ when Type::Nominal then type.class_name == "String"
42
+ when Type::Union then type.members.all? { |member| string_carrier?(member) }
43
+ else false
44
+ end
45
+ end
46
+
47
+ def buffer_argument(call_node)
48
+ arguments = call_node.arguments&.arguments
49
+ return nil if arguments.nil?
50
+
51
+ position = READERS[call_node.name]
52
+ position = 0 if position.nil? && string_io_new?(call_node)
53
+ return nil if position.nil?
54
+
55
+ argument = arguments[position]
56
+ MutationWidening::ALIAS_ACCESSORS.key?(argument.class) ? argument : nil
57
+ end
58
+
59
+ def string_io_new?(call_node)
60
+ return false unless call_node.name == :new
61
+
62
+ receiver = call_node.receiver
63
+ case receiver
64
+ when Prism::ConstantReadNode then receiver.name == :StringIO
65
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :StringIO
66
+ else false
67
+ end
68
+ end
69
+ private_class_method :buffer_argument, :string_io_new?, :string_carrier?
70
+ end
71
+ end
72
+ end
@@ -95,11 +95,10 @@ module Rigor
95
95
  # may type in a later round once its own argument resolves). Unresolved call sites do not
96
96
  # contribute.
97
97
  #
98
- # What it does NOT do yet (deferred — the check-wiring slice): feeding the table into the
99
- # `check` walk (only `coverage --protection` consumes it today), keyword / optional / rest
100
- # / block parameters, inherited-method receivers, top-level helpers, and a rigorous
101
- # closed-call-site-set proof (the union is optimistic over resolved sites — acceptable
102
- # because the only consumer is the protection metric, which runs no diagnostics).
98
+ # What it does NOT do yet: typing keyword / optional / rest / block parameters themselves,
99
+ # inherited-method receivers, top-level helpers, and a rigorous closed-call-site-set proof
100
+ # (the union is optimistic over resolved sites; the `check` walk tolerates that because an
101
+ # inferred parameter is only a lower bound, which the in-body rules decline on, WD6b).
103
102
  class ParameterInferenceCollector
104
103
  EMPTY = {}.freeze
105
104
 
@@ -120,7 +119,7 @@ module Rigor
120
119
  # @param files — project `.rb` paths to scan for call sites.
121
120
  # @param target_ruby — Prism parse target.
122
121
  # @param max_rounds — the WD5 fixpoint cap (1 = single-level).
123
- # @return => Hash{Symbol => Rigor::Type}}] frozen.
122
+ # @return the inferred parameter table, frozen.
124
123
  def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
125
124
  new(files: files, environment: environment, target_ruby: target_ruby,
126
125
  max_rounds: max_rounds, workers: workers).collect
@@ -13,7 +13,7 @@ module Rigor
13
13
  #
14
14
  # ADR-17 slice 2 gave `pre_eval:` a project-wide **method** registry ({ProjectPatchedMethods}). A
15
15
  # constant declared in the same file stayed invisible across the file boundary: `Scope#in_source_constants`
16
- # is per-file and is deliberately not part of `Runner#project_scope_seed_tables`, so only class-shaped
16
+ # was per-file and kept out of `Runner#project_scope_seed_tables`, so only class-shaped
17
17
  # constants (a `class` / `module` declaration, or one of the four meta-new forms `ScopeIndexer`'s
18
18
  # `record_class_new_constant_decl` promotes) ever crossed. `TIMEOUT = 30` in a listed file read as
19
19
  # `Dynamic[top]` everywhere else.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "external_ancestor_resolution"
5
+ require_relative "../run_scoped_slots"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -19,7 +20,7 @@ module Rigor
19
20
  NO_TARGETS = [].freeze
20
21
  private_constant :NO_TARGETS
21
22
 
22
- MEMO_KEY = :__rigor_project_method_ownership_memo__
23
+ MEMO_KEY = RunScopedSlots.register(:__rigor_project_method_ownership_memo__)
23
24
  private_constant :MEMO_KEY
24
25
 
25
26
  module_function
@@ -72,13 +73,15 @@ module Rigor
72
73
  # `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
73
74
  # an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
74
75
  # workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
75
- # belongs to {ClosureEscapeAnalyzer}'s ancestry step.
76
+ # belongs to {ClosureEscapeAnalyzer}'s ancestry step. The `:block_runs` bucket belongs to
77
+ # {BlockRunCount}.
76
78
  def memo(scope)
77
79
  discovery = scope.discovery
78
80
  environment = scope.environment
79
81
  slot = Thread.current[MEMO_KEY]
80
82
  unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
81
- slot = [discovery, environment, { instance: {}, singleton: {}, ancestry: {} }]
83
+ buckets = { instance: {}, singleton: {}, ancestry: {}, block_runs: {}.compare_by_identity }
84
+ slot = [discovery, environment, buckets]
82
85
  Thread.current[MEMO_KEY] = slot
83
86
  end
84
87
  slot[2]
@@ -39,7 +39,8 @@ module Rigor
39
39
  # `dependencies.source_inference:`), and no `discovery_seed:` base is merged in (the runner can start from one).
40
40
  NOT_SEEDED = {
41
41
  run_generation: "the identity token of a check run's return memo; these paths run no check",
42
- constant_sources: "read only by dependency recording, which only an incremental check run turns on",
42
+ constant_sources: "read by dependency recording and the #663 pre_eval retraction; a seed carries it only " \
43
+ "when asked (DiscoverySeed.tables_for_buffer(constant_sources: true), the editor publish)",
43
44
  param_inferred_types: "the parameter-inference output itself; each caller adds its own " \
44
45
  "(DiscoverySeed.build, coverage under parameter_inference:, the collector's last round)"
45
46
  }.freeze
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "block_run_count"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2127 — whether a call to a PROJECT method may run the block it passes more than once, read off the
11
+ # callee's body. {BlockRepetition.may_repeat?} answers only for the core iterators {ClosureEscapeAnalyzer}
12
+ # catalogues, so `def yielder = (yield; yield)` or the idiomatic wrapper `def each = @items.each { |i| yield i }`
13
+ # counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
14
+ # false if (seen += 1) > 3 }`) never joined the method's return type.
15
+ #
16
+ # Only the later-run `return` pass asks {.may_run_block_repeatedly?}
17
+ # (`StatementEvaluator#collect_repeated_block_returns`); the block's entry scopes and the ADR-56 write-back keep
18
+ # {BlockRepetition}'s rule. That pass only widens the method's return type, so the rule here errs toward
19
+ # "repeats". The expression-position block-value pass asks {.block_value_may_repeat?} instead, whose rule
20
+ # ({BlockRunCount}) errs the other way. The callee repeats when its body, outside a nested `def` (a `yield` in a
21
+ # class body is a syntax error inside a method):
22
+ #
23
+ # - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
24
+ # parameter), or one inside a loop, a block or a lambda, which may run it many times; or
25
+ # - hands the block on (`each(&blk)`, `each(&)`, `...` forwarding) or reads the `&blk` parameter any other way
26
+ # (stores it, passes it as an argument), so code the walk does not see may run it any number of times.
27
+ #
28
+ # A callee the analysis cannot resolve to a project `def` (an unknown receiver, a method defined by
29
+ # metaprogramming) answers false, as before.
30
+ module ProjectYielder
31
+ module_function
32
+
33
+ BLOCK_RUN_CALLS = Set[:call, :yield, :===, :[]].freeze
34
+ REPEATING_NODES = Set[Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode,
35
+ Prism::LambdaNode].freeze
36
+ private_constant :BLOCK_RUN_CALLS, :REPEATING_NODES
37
+
38
+ def may_run_block_repeatedly?(call_node, receiver_type, scope)
39
+ def_node = callee_def(call_node, receiver_type, scope)
40
+ return false unless def_node.is_a?(Prism::DefNode) && def_node.body
41
+
42
+ repeats?(def_node)
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ # Issues #2301 and #2303 — whether the callee may run the block more than once on one path, for the
48
+ # expression-position block-value pass ({BlockRunCount}). Answers false where the callee does not resolve.
49
+ def block_value_may_repeat?(call_node, receiver_type, scope)
50
+ def_node = callee_def(call_node, receiver_type, scope)
51
+ return false unless def_node.is_a?(Prism::DefNode) && def_node.body
52
+
53
+ BlockRunCount.may_repeat?(def_node, scope)
54
+ rescue StandardError
55
+ false
56
+ end
57
+
58
+ # The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
59
+ # top-level method resolves to.
60
+ def callee_def(call_node, receiver_type, scope)
61
+ name = call_node.name
62
+ found =
63
+ case receiver_type
64
+ when Type::Nominal then scope.user_def_through_ancestors(receiver_type.class_name, name).first
65
+ when Type::Singleton then scope.singleton_def_for(receiver_type.class_name, name)
66
+ end
67
+ found || (call_node.receiver.nil? ? scope.top_level_def_for(name) : nil)
68
+ end
69
+
70
+ def repeats?(def_node)
71
+ block_param = def_node.parameters&.block
72
+ counter = { sites: 0, repeats: false }
73
+ walk(def_node.body, block_param&.name, false, counter)
74
+ counter[:repeats] || counter[:sites] >= 2
75
+ end
76
+
77
+ def walk(node, block_name, repeating, counter)
78
+ return if counter[:repeats] || node.is_a?(Prism::DefNode)
79
+
80
+ skipped = visit(node, block_name, repeating, counter)
81
+ inner = repeating || REPEATING_NODES.include?(node.class)
82
+ node.compact_child_nodes.each do |child|
83
+ walk(child, block_name, inner, counter) unless child.equal?(skipped)
84
+ end
85
+ end
86
+
87
+ # Counts `node` and answers the child the walk must not visit: the `blk` receiver of a `blk.call`, which is a
88
+ # run, not a hand-off.
89
+ def visit(node, block_name, repeating, counter)
90
+ case node
91
+ when Prism::YieldNode then run_site(repeating, counter)
92
+ when Prism::ForwardingArgumentsNode then counter[:repeats] = true
93
+ when Prism::BlockArgumentNode then counter[:repeats] = true if forwards_block?(node, block_name)
94
+ when Prism::CallNode
95
+ return unless block_run_call?(node, block_name)
96
+
97
+ run_site(repeating, counter)
98
+ node.receiver
99
+ when Prism::LocalVariableReadNode then counter[:repeats] = true if block_name && node.name == block_name
100
+ end
101
+ end
102
+
103
+ def run_site(repeating, counter)
104
+ counter[:sites] += 1
105
+ counter[:repeats] = true if repeating
106
+ end
107
+
108
+ # `blk.call` and its spellings.
109
+ def block_run_call?(node, block_name)
110
+ receiver = node.receiver
111
+ !block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
112
+ BLOCK_RUN_CALLS.include?(node.name)
113
+ end
114
+
115
+ # `&blk` or an anonymous `&` handed to another call.
116
+ def forwards_block?(node, block_name)
117
+ expression = node.expression
118
+ expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -85,6 +85,26 @@ module Rigor
85
85
  candidates(first, depth + 1) + candidates(second, depth + 1)
86
86
  end
87
87
 
88
+ # Calls whose result hands its caller the receiver's own element objects (issue #2287): `h.values` the hash's
89
+ # values, `a.dup` / `a.clone` a shallow copy sharing them, and the blockless enumerator spellings (`a.each`,
90
+ # `a.map`, `a.each_with_index`, `e.with_index`) the elements themselves.
91
+ ELEMENT_EXPOSING_CALLS = %i[values dup clone each map each_with_index with_index].to_set.freeze
92
+ private_constant :ELEMENT_EXPOSING_CALLS
93
+
94
+ # The variables whose elements a loop over `receiver` is handed: {.candidates} of `receiver` once every
95
+ # blockless {ELEMENT_EXPOSING_CALLS} call is peeled off, so `h.values.each { |v| v << 2 }` and
96
+ # `a.each_with_index.map { |x, i| x << i }` name `h` and `a`. Only a loop's element widening reads it; an
97
+ # in-place mutation OF such a result (`h.values << 1`, `a.dup << 1`) changes a new object, not the variable.
98
+ def element_sources(receiver)
99
+ depth = 0
100
+ while receiver.is_a?(Prism::CallNode) && receiver.block.nil? && ELEMENT_EXPOSING_CALLS.include?(receiver.name)
101
+ return [] if (depth += 1) > WALK_DEPTH_CAP
102
+
103
+ receiver = receiver.receiver
104
+ end
105
+ candidates(receiver)
106
+ end
107
+
88
108
  # The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
89
109
  # variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
90
110
  # (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../run_scoped_slots"
4
+
3
5
  module Rigor
4
6
  module Inference
5
7
  module RefinedDispatch
@@ -16,7 +18,7 @@ module Rigor
16
18
  # the name itself, and no constant write of it. Any other shape answers false, as does a class a
17
19
  # `K = Class.new(...)` write names.
18
20
  module ConstructedClass
19
- MEMO_KEY = :__rigor_constructed_class__
21
+ MEMO_KEY = RunScopedSlots.register(:__rigor_constructed_class__)
20
22
  EMPTY = [].freeze
21
23
  private_constant :MEMO_KEY, :EMPTY
22
24
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../builtins/cruby_definers"
4
4
  require_relative "constructed_class"
5
+ require_relative "../../run_scoped_slots"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -317,7 +318,7 @@ module Rigor
317
318
  all.include?(name) ? all - [name] : all
318
319
  end
319
320
 
320
- SUBCLASS_MEMO_KEY = :__rigor_own_method_subclasses__
321
+ SUBCLASS_MEMO_KEY = RunScopedSlots.register(:__rigor_own_method_subclasses__)
321
322
  private_constant :SUBCLASS_MEMO_KEY
322
323
 
323
324
  # One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only