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
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../source/node_walker"
5
+ require_relative "../source/node_children"
6
+ require_relative "../run_scoped_slots"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2555 L3 — which locals of a body may have ESCAPED, so a write to them never carries the `:synthesised`
11
+ # construction fact ({Scope#construction_fact?}).
12
+ #
13
+ # The fact lets the mutation seams re-grow a carrier without the `Dynamic[top]` floor, and the floor existed
14
+ # because a store the seam does not see leaves the carrier stale. A store reaches a local's object unseen only
15
+ # through another holder of it, and almost any position can make one: an alias, a container, an argument (a
16
+ # constructor or an attribute writer keeps it, `Array(a)` and `ident(a)` return it), a call that returns the
17
+ # receiver for some class (`Set#merge`, `a.each`), or a method the project defines. So every reference escapes
18
+ # unless it is in one of these positions:
19
+ #
20
+ # - the receiver of a call on the per-class {READERS} list, which neither returns, keeps nor mutates its receiver;
21
+ # - the receiver of an {ITERATORS} call or of a mutator the seams model ({.mutators_for}), with the call's value
22
+ # discarded;
23
+ # - the body's value position (its last statement, or `return a`), after which nothing in the body runs;
24
+ # - a bare read whose value is discarded.
25
+ #
26
+ # A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
27
+ # class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
28
+ # any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
29
+ # body that calls `binding`, `eval`, `local_variable_set`, `instance_variable_set` or a string-form
30
+ # `instance_eval` / `class_eval` / `module_eval`.
31
+ # The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
32
+ # the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
33
+ # scanned too, though its locals are its own; a same-named local there can only over-report an escape.
34
+ module ConstructionEscape
35
+ CLASSES = %w[Array Hash Set].freeze
36
+
37
+ # The calls that answer a value other than the receiver, keep no reference to it, and change nothing in it.
38
+ READERS = Ractor.make_shareable(
39
+ {
40
+ "Array" => %i[
41
+ size length count empty? any? none? all? include? first last [] dig fetch index find_index sum min max
42
+ join dup map select filter reject sort sort_by uniq compact flatten reverse take drop to_h to_set ==
43
+ ].to_set,
44
+ "Hash" => %i[
45
+ size length count empty? any? include? key? has_key? member? value? has_value? [] dig fetch keys values
46
+ dup map select filter reject to_a ==
47
+ ].to_set,
48
+ "Set" => %i[size length count empty? any? include? member? first dup map select filter reject to_a ==].to_set
49
+ }
50
+ )
51
+
52
+ # The calls that answer the receiver itself and yield only its members: safe with their value discarded.
53
+ ITERATORS = Ractor.make_shareable(
54
+ {
55
+ "Array" => %i[each each_with_index each_index reverse_each].to_set,
56
+ "Hash" => %i[each each_pair each_key each_value each_with_index].to_set,
57
+ "Set" => %i[each].to_set
58
+ }
59
+ )
60
+
61
+ # Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
62
+ FRAME_CALLS = %i[binding eval local_variable_set instance_variable_set].to_set.freeze
63
+ STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
64
+
65
+ CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
66
+ LOCAL_REFERENCES = [
67
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
68
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
69
+ ].freeze
70
+ LOCAL_WRITES = [
71
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
72
+ Prism::LocalVariableAndWriteNode
73
+ ].freeze
74
+ ALL = :all
75
+ EMPTY_CALLS = Set.new.freeze
76
+ # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so a long-lived process never serves a verdict
77
+ # from a body an earlier run parsed.
78
+ CACHE_KEY = RunScopedSlots.register(:__rigor_construction_escape)
79
+ private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE_KEY,
80
+ :STRING_EVAL_CALLS
81
+
82
+ # One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
83
+ Result = Data.define(:escapes, :calls)
84
+ private_constant :Result
85
+
86
+ module_function
87
+
88
+ # True when the local `name`, bound to a carrier of `class_name`, may escape somewhere in `body`, or when
89
+ # `body` is unknown. `scope` answers whether the project defines one of the admitted calls on the class
90
+ # ({.project_defines?}).
91
+ def escaped?(body, name, class_name, scope)
92
+ return true if body.nil? || class_name.nil?
93
+
94
+ table = (Thread.current[CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
95
+ result = table[body] ||= scan(body)
96
+ return true if result.equal?(ALL)
97
+
98
+ reason = result.escapes[name]
99
+ return true if !reason.nil? && (reason.equal?(ALL) || reason.include?(class_name))
100
+
101
+ result.calls.fetch(name, EMPTY_CALLS).any? { |call| project_defines?(scope, class_name, call) }
102
+ end
103
+
104
+ # Whether the project defines a method named `call` anywhere ({Scope#project_method_name?}): on any class or
105
+ # module, instance or singleton side, in a `refine` block, in any file. An admitted call of such a name escapes
106
+ # the local whatever the receiver's class, because a prepended or included module, a refinement or a reopen can
107
+ # put that name on Array, Hash or Set, and telling which owner reaches the receiver is not worth the precision
108
+ # it buys: without the fact the seams answer as before. When no def is found the def-node lookup on the class
109
+ # itself still runs, so an incremental run records the missing `Array#size` and a later reopen re-analyses
110
+ # this file.
111
+ def project_defines?(scope, class_name, call)
112
+ scope.project_method_name?(call) || !scope.user_def_for(class_name, call).nil?
113
+ end
114
+
115
+ # The receiver mutators the seams model for `class_name` ({MutationWidening}, {HashLookupMutation},
116
+ # {SetMutation}).
117
+ def mutators_for(class_name)
118
+ case class_name
119
+ when "Array" then MutationWidening::ARRAY_MUTATORS
120
+ when "Hash" then MutationWidening::HASH_MUTATORS | HashLookupMutation::MUTATORS
121
+ else SetMutation::MUTATORS
122
+ end
123
+ end
124
+
125
+ def scan(body)
126
+ escapes = {}
127
+ calls = {}
128
+ Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
129
+ return ALL if frame_call?(node)
130
+ next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
131
+
132
+ record(escapes, calls, node, ancestors)
133
+ end
134
+ Result.new(escapes: escapes.freeze, calls: calls.freeze)
135
+ end
136
+
137
+ def record(escapes, calls, node, ancestors)
138
+ name = node.name
139
+ call = receiver_call(node, ancestors)
140
+ unsafe =
141
+ if call
142
+ unsafe_classes(call, ancestors).tap { |u| (calls[name] ||= Set.new) << call.name if u.size < CLASSES.size }
143
+ else
144
+ escape_reference?(node, ancestors) ? CLASSES : []
145
+ end
146
+ return if unsafe.empty? || escapes[name].equal?(ALL)
147
+
148
+ escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
149
+ end
150
+
151
+ def frame_call?(node)
152
+ return false unless node.is_a?(Prism::CallNode)
153
+
154
+ FRAME_CALLS.include?(node.name) || (STRING_EVAL_CALLS.include?(node.name) && !node.arguments.nil?)
155
+ end
156
+
157
+ # The call a read outside any closure is the receiver of, or nil.
158
+ def receiver_call(node, ancestors)
159
+ return nil unless node.is_a?(Prism::LocalVariableReadNode) && !in_closure?(ancestors)
160
+
161
+ parent = ancestors.last
162
+ parent if parent.is_a?(Prism::CallNode) && parent.receiver.equal?(node)
163
+ end
164
+
165
+ # The classes for which `call` on the local may hand it on.
166
+ def unsafe_classes(call, ancestors)
167
+ discarded = !value_used?(ancestors, ancestors.size - 2, call)
168
+ CLASSES.reject do |class_name|
169
+ READERS.fetch(class_name).include?(call.name) ||
170
+ (discarded && (ITERATORS.fetch(class_name).include?(call.name) ||
171
+ mutators_for(class_name).include?(call.name)))
172
+ end
173
+ end
174
+
175
+ def in_closure?(ancestors)
176
+ ancestors.any? { |a| CLOSURES.any? { |k| a.is_a?(k) } }
177
+ end
178
+
179
+ # Whether a reference that is no call's receiver may hand the object on.
180
+ def escape_reference?(node, ancestors)
181
+ return true if in_closure?(ancestors)
182
+ return value_used?(ancestors, ancestors.size - 1, node) if LOCAL_WRITES.any? { |k| node.is_a?(k) }
183
+ return false unless node.is_a?(Prism::LocalVariableReadNode)
184
+
185
+ parent = ancestors.last
186
+ return false if parent.is_a?(Prism::ArgumentsNode) && ancestors[-2].is_a?(Prism::ReturnNode)
187
+
188
+ !(parent.is_a?(Prism::StatementsNode) && !value_used?(ancestors, ancestors.size - 1, node))
189
+ end
190
+
191
+ # Whether the value of `node`, whose parent is `ancestors[index]`, reaches anything. The body's own value (its
192
+ # last statement) is the return position, after which nothing in the body runs, so it reads as unused.
193
+ def value_used?(ancestors, index, node)
194
+ while index >= 0
195
+ parent = ancestors[index]
196
+ case parent
197
+ when Prism::StatementsNode
198
+ return false unless parent.body.last.equal?(node)
199
+ when Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode
200
+ return true if parent.predicate.equal?(node)
201
+ when Prism::WhenNode, Prism::InNode
202
+ return true unless parent.statements.equal?(node)
203
+ when Prism::WhileNode, Prism::UntilNode, Prism::EnsureNode
204
+ return !parent.statements.equal?(node)
205
+ when Prism::ElseNode, Prism::BeginNode, Prism::ParenthesesNode, Prism::ProgramNode, Prism::RescueNode
206
+ nil
207
+ else
208
+ return true
209
+ end
210
+ node = parent
211
+ index -= 1
212
+ end
213
+ false
214
+ end
215
+ end
216
+ end
217
+ end
@@ -235,7 +235,7 @@ module Rigor
235
235
  # A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
236
236
  # each collector alone (a pair in which one declines), the run without each collector (a larger run in
237
237
  # which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
238
- # them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
238
+ # them. `kind` names the table; `key` is the collector, or the rule's Symbol for a split.
239
239
  def subset(kind, collector = nil)
240
240
  table = (@subsets[kind] ||= {}.compare_by_identity)
241
241
  key = collector || kind
@@ -5,6 +5,7 @@ require_relative "../type"
5
5
  require_relative "../source/node_walker"
6
6
  require_relative "mutation_rejoin"
7
7
  require_relative "receiver_alias"
8
+ require_relative "set_mutation"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -54,11 +55,31 @@ module Rigor
54
55
  # and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
55
56
  # the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
56
57
  # an element passed to a call (`add(x)`), which the straight-line widening does not either.
57
- def element_mutation?(body, names)
58
+ #
59
+ # A Set-only mutator (`add`, `merge`, `subtract`, … — {SetMutation::MUTATORS} less `SHAPE_MUTATORS`) counts
60
+ # only when `set_held` is given and answers true (issue #2363). The names are read without their receiver,
61
+ # and `merge` on a Hash element returns a new Hash, so the caller answers from the collection's type whether
62
+ # an element could be a Set at all ({holds_set?}). The block runs at the first such site, at most once.
63
+ def element_mutation?(body, names, &set_held)
58
64
  return false if body.nil?
59
65
 
60
66
  names |= nested_parameter_names(body)
61
- Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names) }
67
+ Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names, set_held) }
68
+ end
69
+
70
+ # True when `type` holds a `Set` at some depth: as itself, a union member, a `Tuple` element, a `HashShape`
71
+ # value or a type argument. A collection whose type holds none has no element a Set-only mutator can be
72
+ # mutating, so a `merge` on one of its elements is `Hash#merge`, which returns a new Hash.
73
+ def holds_set?(type)
74
+ case type
75
+ when Type::Constant then SetMutation.constant?(type)
76
+ when Type::Nominal then type.class_name == "Set" || type.type_args.any? { |arg| holds_set?(arg) }
77
+ when Type::Tuple then type.elements.any? { |element| holds_set?(element) }
78
+ when Type::HashShape then type.pairs.each_value.any? { |value| holds_set?(value) }
79
+ when Type::Union then type.members.any? { |member| holds_set?(member) }
80
+ when Type::Difference then holds_set?(type.base)
81
+ else false
82
+ end
62
83
  end
63
84
 
64
85
  # The names `parameters` (a block's parameter list, or a `for` index) binds.
@@ -80,9 +101,12 @@ module Rigor
80
101
  end
81
102
  end
82
103
 
83
- def element_mutation_site?(node, names)
104
+ def element_mutation_site?(node, names, set_held)
84
105
  if node.is_a?(Prism::CallNode)
85
- MutationWidening::SHAPE_MUTATORS.include?(node.name) && element_rooted?(node.receiver, names)
106
+ return false unless element_rooted?(node.receiver, names)
107
+
108
+ MutationWidening::SHAPE_MUTATORS.include?(node.name) ||
109
+ (!set_held.nil? && SetMutation::MUTATORS.include?(node.name) && set_held.call)
86
110
  else
87
111
  ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
88
112
  end
@@ -101,15 +125,23 @@ module Rigor
101
125
  # A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
102
126
  # place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
103
127
  # (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
104
- # value does ({StatementEvaluator#for_collection_after}).
128
+ # value does ({StatementEvaluator#for_collection_after}). A receiver that is a call exposing a variable's
129
+ # elements (`h.values`, `a.dup`, `a.each_with_index`) names that variable ({ReceiverAlias.element_sources}).
105
130
  def widen_element_aliases(receiver, body, names, scope)
106
- return scope unless element_mutation?(body, names)
131
+ return scope unless element_mutation?(body, names) { receiver_holds_set?(receiver, scope) }
107
132
 
108
133
  widen_receiver(receiver, scope)
109
134
  end
110
135
 
136
+ def receiver_holds_set?(receiver, scope)
137
+ ReceiverAlias.element_sources(receiver).any? do |read|
138
+ getter, = MutationWidening::ALIAS_ACCESSORS[read.class]
139
+ getter && holds_set?(scope.public_send(getter, ReceiverAlias.read_name(read)))
140
+ end
141
+ end
142
+
111
143
  def widen_receiver(receiver, scope)
112
- ReceiverAlias.candidates(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
144
+ ReceiverAlias.element_sources(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
113
145
  end
114
146
 
115
147
  # `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
@@ -8,7 +8,7 @@ module Rigor
8
8
  # file's methods: the file was never parsed this run, so there is no live node to store. A handle records
9
9
  # the four things needed to resolve, fingerprint, or scope it without the node:
10
10
  #
11
- # - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
11
+ # - `path` + `node_id` — the resolution key. The per-run parse memo ({DefNodeResolver.resolve}) parses the
12
12
  # file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
13
13
  # repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
14
14
  # from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
@@ -104,7 +104,7 @@ module Rigor
104
104
  # is recorded like any other: it is the bundle saying the def is written at the top level, and dropping
105
105
  # it would leave the warm path peeling the caller's namespace where a cold run resolves at the top
106
106
  # level. Only `nil` — a bundle that recorded nothing for the def — is skipped, and
107
- # `Cache::IncrementalSnapshot::SCHEMA` 17 is what keeps a pre-#716 blob (where `nil` MEANT top level)
107
+ # the `SCHEMA` bump to 17 (`Cache::IncrementalSnapshot`) keeps a pre-#716 blob, where `nil` MEANT top level,
108
108
  # from being read as one.
109
109
  def self.record_nesting(memo, node, nesting)
110
110
  return if node.nil? || nesting.nil?
@@ -35,7 +35,7 @@ module Rigor
35
35
  body = def_node.body
36
36
  return nil if body.nil?
37
37
 
38
- last = body_last_expression(body)
38
+ last = primary_value_expression(body)
39
39
  return nil if last.nil?
40
40
 
41
41
  inner_scope = scope_index[last] || scope_index[body] || scope_index[def_node]
@@ -46,11 +46,54 @@ module Rigor
46
46
 
47
47
  lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
48
48
  last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
49
- union_with_explicit_returns(body, last_type, scope_index, lookup_context: lookup_context)
49
+ arm_types = rescue_arm_types(body, inner_scope, scope_index, lookup_context)
50
+ return nil if arm_types.nil?
51
+
52
+ union_with_explicit_returns(body, Type::Combinator.union(last_type, *arm_types), scope_index,
53
+ lookup_context: lookup_context)
50
54
  rescue StandardError
51
55
  nil
52
56
  end
53
57
 
58
+ # Issue #2410 — the value a def body ends on when no exception escapes its primary path. A def-level
59
+ # `rescue … else … end` body is a `BeginNode` whose else-clause, when present, replaces the body's value (the
60
+ # ensure-clause's value is discarded, so it never counts).
61
+ def primary_value_expression(body)
62
+ return body_last_expression(body) unless body.is_a?(Prism::BeginNode) && body.else_clause
63
+
64
+ body_last_expression(body.else_clause.statements)
65
+ end
66
+
67
+ # Issue #2410 — the value of each arm of a def-level rescue chain, which the method returns when that arm
68
+ # completes. An empty arm returns `nil`; an arm ending in `raise`, `retry` or `return` types as bottom and so
69
+ # adds nothing here (a `return`'s value is collected with the other explicit returns). Nil when an arm cannot
70
+ # be typed: the body's value alone would be a narrower, wrong contract.
71
+ def rescue_arm_types(body, inner_scope, scope_index, lookup_context)
72
+ return [] unless body.is_a?(Prism::BeginNode)
73
+
74
+ types = []
75
+ arm = body.rescue_clause
76
+ while arm
77
+ type = rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
78
+ return nil if type.nil?
79
+
80
+ types << type
81
+ arm = arm.subsequent
82
+ end
83
+ types
84
+ end
85
+
86
+ def rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
87
+ value = body_last_expression(arm.statements)
88
+ return Type::Combinator.constant_of(nil) if value.nil?
89
+
90
+ scope = scope_index[value] || scope_index[arm] || inner_scope
91
+ type = safe_type_of(scope, value)
92
+ return nil if type.nil?
93
+
94
+ lookup_context ? with_lookup_nil(type, value, scope, lookup_context) : type
95
+ end
96
+
54
97
  # What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
55
98
  # under a guard the source of the guarding predicates ({#truthy_guards}).
56
99
  LookupContext = Data.define(:scope_index, :guards)
@@ -227,8 +270,7 @@ module Rigor
227
270
 
228
271
  scope = scope_index[return_node] || scope_index[args.first]
229
272
  return if scope.nil?
230
- # `return a, b` packs into a Tuple at runtime; the MVP only handles the single-value form. Multi-arg
231
- # returns contribute no type to keep the implementation focused.
273
+ # `return a, b` packs into a Tuple at runtime; only the single-value form is typed.
232
274
  return unless args.size == 1
233
275
 
234
276
  type = safe_type_of(scope, args.first)
@@ -25,8 +25,10 @@ module Rigor
25
25
  # truth-tested: {UNKNOWN} and {ABSENT} are both truthy, so any other use folds a decline into a firing arm.
26
26
  # `spec/rigor/inference/definer_resolution_case_in_spec.rb` fails on a call site of any other shape.
27
27
  #
28
- # It migrates one question at a time: `SourceArity` (C1b) and the relationship lints (C1c) read the instance
29
- # side; no production reader reads the singleton side yet (C2-a landed it for C2's typing sites).
28
+ # It migrates one question at a time: `SourceArity` (C1b, C2-c) asks for the receiver's own side, instance or
29
+ # singleton; the override lints (C1c) and the C2 typing sites (`ExpressionTyper#resolve_typing_def_with_owner`,
30
+ # `StatementEvaluator#own_definer_node`) read the instance side; `RefinedDispatch::OwnMethod.certain_owner?` and
31
+ # `CheckRules#reached_definer_visibility` pass the side they hold.
30
32
  module DefinerResolution
31
33
  Known = Data.define(:answer, :owner)
32
34
  UnknownResult = Data.define
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../run_scoped_slots"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The `method_name (Symbol) → [owner names]` inverted index the override gate reads
8
+ # (`ExpressionTyper#method_definers_index`), over one instance or singleton def table.
9
+ #
10
+ # Issue #2300 — a file's def table is its own `def`s merged over the cross-file seed
11
+ # (`ScopeIndexer#merge_def_node_tables`, `#merge_singleton_def_nodes`, then the #526 extend fold), a new Hash per
12
+ # file. Inverting it whole scanned every project `def` once per analysed file. {.record_layers} remembers the
13
+ # seed a final merged table was built over, and {.for} then inverts the seed ONCE per seed table and, per file,
14
+ # only the owners whose inner table is not the seed's own object ({Layered}).
15
+ #
16
+ # The two answers hold the same owners: an owner the merge left alone shares the seed's inner table, so its
17
+ # names are the seed's, and every other owner is read from the merged table itself. That holds only while the
18
+ # seed cannot change under the cached index, so a seed whose tables are not all frozen, or a merged table
19
+ # missing one of the seed's owners, is inverted whole, as is a table {.record_layers} was not told about. Only
20
+ # the order of a name's owners can differ, and the gate asks `any?` of the list.
21
+ module DefinersIndex
22
+ # These thread slots are bounded, like `ExpressionTyper#method_definers_index`'s two ways. Each is a fixed
23
+ # number of entries, replaced rather than accumulated: two `[merged, seed]` pairs (the last file's two def
24
+ # tables) and two seed indexes. The run that filled them releases them when it ends ({RunScopedSlots}, #2399),
25
+ # so a finished run keeps none of its seed tables or their indexes alive.
26
+ LAYERS_KEY = RunScopedSlots.register(:__rigor_definers_index_layers__)
27
+ SEED_INDEX_KEY = RunScopedSlots.register(:__rigor_definers_index_seed__)
28
+ SEED_INDEX_ALT_KEY = RunScopedSlots.register(:__rigor_definers_index_seed_alt__)
29
+ private_constant :LAYERS_KEY, :SEED_INDEX_KEY, :SEED_INDEX_ALT_KEY
30
+
31
+ # A name's owners: the seed's, less the owners the file replaced, plus the replaced owners' own.
32
+ class Layered
33
+ def initialize(seed_index, replaced, replaced_index)
34
+ @seed_index = seed_index
35
+ @replaced = replaced
36
+ @replaced_index = replaced_index
37
+ @answers = {}
38
+ end
39
+
40
+ def [](method_name)
41
+ return @answers[method_name] if @answers.key?(method_name)
42
+
43
+ from_seed = @seed_index[method_name]
44
+ from_seed = from_seed.reject { |owner| @replaced.key?(owner) } if from_seed && !@replaced.empty?
45
+ from_file = @replaced_index[method_name]
46
+ owners = from_file.nil? ? from_seed : (from_seed || []) + from_file
47
+ @answers[method_name] = owners.nil? || owners.empty? ? nil : owners
48
+ end
49
+ end
50
+
51
+ module_function
52
+
53
+ # Records that the final `merged` table was built over `seed`. The last two are kept (a file merges an
54
+ # instance and a singleton table), replaced rather than accumulated.
55
+ def record_layers(merged, seed)
56
+ previous = Thread.current[LAYERS_KEY]
57
+ Thread.current[LAYERS_KEY] = [[merged, seed], previous&.first].compact
58
+ end
59
+
60
+ # The definers index over `table`.
61
+ def for(table)
62
+ seed = Thread.current[LAYERS_KEY]&.find { |entry| entry[0].equal?(table) }&.last
63
+ return build(table) if seed.nil? || !deeply_frozen?(seed)
64
+
65
+ replaced = {}
66
+ kept = 0
67
+ table.each do |owner, methods|
68
+ if methods.equal?(seed[owner])
69
+ kept += 1
70
+ else
71
+ replaced[owner] = methods
72
+ end
73
+ end
74
+ return build(table) unless kept + replaced.count { |owner, _| seed.key?(owner) } == seed.size
75
+
76
+ Layered.new(seed_index(seed), replaced, build(replaced))
77
+ end
78
+
79
+ def deeply_frozen?(seed)
80
+ seed.frozen? && seed.each_value.all?(&:frozen?)
81
+ end
82
+
83
+ # The seed's index, built once per seed table. Two ways, an instance and a singleton seed table.
84
+ def seed_index(seed)
85
+ [SEED_INDEX_KEY, SEED_INDEX_ALT_KEY].each do |key|
86
+ slot = Thread.current[key]
87
+ return slot[1] if slot && slot[0].equal?(seed)
88
+ end
89
+ index = build(seed)
90
+ Thread.current[SEED_INDEX_ALT_KEY] = Thread.current[SEED_INDEX_KEY]
91
+ Thread.current[SEED_INDEX_KEY] = [seed, index]
92
+ index
93
+ end
94
+
95
+ def build(table)
96
+ index = {}
97
+ table.each do |class_name, methods|
98
+ next if class_name == ScopeIndexer::TOP_LEVEL_DEF_KEY
99
+
100
+ methods.each_key { |method_name| (index[method_name] ||= []) << class_name }
101
+ end
102
+ index
103
+ end
104
+ end
105
+ end
106
+ end