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
@@ -12,6 +12,7 @@ require_relative "match_rebinding/frame"
12
12
  require_relative "match_rebinding/operands"
13
13
  require_relative "match_rebinding/calls"
14
14
  require_relative "match_rebinding/self_calls"
15
+ require_relative "match_rebinding/yielding_match"
15
16
  require_relative "last_line"
16
17
 
17
18
  module Rigor
@@ -367,12 +368,21 @@ module Rigor
367
368
  #
368
369
  # Issue #1429 — the entry also restores a guard's narrowing of a global or constant where the body may run after
369
370
  # code that rebinds it ({GuardRebinding.block_entry}).
371
+ #
372
+ # Issue #2443 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, …) on a String or
373
+ # untyped receiver enters with that call's own match edge, laid over the entry above ({YieldingMatch}); a match
374
+ # its body runs still rebinds them through the statement-level rules.
370
375
  def block_entry(scope, block_node, call_node = nil)
376
+ operand_scope = scope
371
377
  scope = GuardRebinding.block_entry(scope, block_node, call_node)
372
378
  return FreshFrameBlocks.entry(scope, call_node) if FreshFrameBlocks.fresh_entry?(call_node, scope)
373
379
 
374
380
  scope = FreshFrameBlocks.closure_entry(scope, block_node, call_node)
375
381
  scope = LastLine.block_entry(scope, block_node, call_node)
382
+ YieldingMatch.entry(frame_entry(scope, block_node, call_node), block_node, call_node, operand_scope)
383
+ end
384
+
385
+ def frame_entry(scope, block_node, call_node)
376
386
  return scope unless scope.match_globals_bound?
377
387
  return scope unless scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
378
388
  (call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
@@ -386,7 +396,7 @@ module Rigor
386
396
 
387
397
  body.is_a?(Prism::Node) && scan(body, scope, base: true)
388
398
  end
389
- private_class_method :entry_may_match?
399
+ private_class_method :frame_entry, :entry_may_match?
390
400
  end
391
401
  end
392
402
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-127 WD3 — the callee floor's memo audit. A memo hit is correct only when the key holds every input the value
6
+ # was computed from, and a key that misses one answers a caller with another caller's value (#2516, #2524). With the
7
+ # audit on, every hit in the callee floor's memos (`resolved_census`, the unrecorded hand-off memo, the successor
8
+ # store and `def_census`) recomputes the value and raises {Mismatch} when it differs from the stored one, so a key
9
+ # defect fails on the first hit instead of waiting for an order that exposes it.
10
+ #
11
+ # Like {BudgetTrace}, a switch for specs and CI (`RIGOR_MEMO_AUDIT=1` in the environment, read once at load, or
12
+ # {enable!}): off by default, a single boolean check when off. A recomputation
13
+ # runs with the audit suspended on its thread and the memos bypassed ({suspended?}), so it reads nothing stored.
14
+ module MemoAudit
15
+ # A memo hit whose stored value differs from a fresh computation.
16
+ class Mismatch < StandardError; end
17
+
18
+ SUSPENDED_KEY = :rigor_memo_audit_suspended
19
+
20
+ @enabled = %w[1 true].include?(ENV["RIGOR_MEMO_AUDIT"].to_s.downcase)
21
+
22
+ module_function
23
+
24
+ def enabled? = @enabled
25
+
26
+ # True while an audit recomputation runs on this thread. The callee floor's memos are then bypassed (read and
27
+ # write), so the recomputation reads no stored entry of another memo: a defect shared across a table (a stale
28
+ # `resolved_census(:edges)` entry) would otherwise make the recomputed value match the stored one.
29
+ def suspended? = Thread.current[SUSPENDED_KEY]
30
+
31
+ def enable! = (@enabled = true)
32
+
33
+ def disable! = (@enabled = false)
34
+
35
+ # `stored`, after checking it against `yield` when the audit is on. `label` and the key parts (`key`, `key2`) name
36
+ # the memo entry in the failure; they are passed apart so an audit that is off allocates nothing.
37
+ def check(label, stored, key, key2 = nil, &)
38
+ return stored unless @enabled && !Thread.current[SUSPENDED_KEY]
39
+
40
+ fresh = suspended(&)
41
+ return stored if fresh == stored
42
+
43
+ raise Mismatch, "#{label} memo hit differs from a fresh computation for #{[key, key2].inspect[0, 120]}: " \
44
+ "stored #{stored.inspect[0, 200]}, fresh #{fresh.inspect[0, 200]}"
45
+ end
46
+
47
+ def suspended
48
+ Thread.current[SUSPENDED_KEY] = true
49
+ yield
50
+ ensure
51
+ Thread.current[SUSPENDED_KEY] = nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -7,7 +7,7 @@ module Rigor
7
7
  # Canonical RBS-core aliases shipped by `core/builtin.rbs` whose body is `<Nominal> | _DuckType`.
8
8
  # Matching an overload against an Integer literal should pick the `(int) -> Array[Elem]` body over
9
9
  # the `(string) -> String` body because Integer satisfies `int`'s strict arm and not `string`'s.
10
- # The translator collapses both aliases to `Dynamic[Top]` (interfaces are not structurally matched
10
+ # The translator reads each alias's interface arm as `Dynamic[Top]` (interfaces are not structurally matched
11
11
  # yet), so a dedicated pass 1.5 between strict and gradual consults this map to pick the alias
12
12
  # whose strict arm matches.
13
13
  #
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # The positional argument lists a call with a precise union argument stands for (#2379): one per combination
9
+ # of the union arguments' members, since the runtime selects an overload per value. `RbsDispatch` selects an
10
+ # overload for each list and joins their returns.
11
+ module ArgumentDistribution
12
+ # The most lists {.lists} spells out when more than one argument is a union. One union argument is linear in
13
+ # its members and is not capped.
14
+ LIMIT = 8
15
+ private_constant :LIMIT
16
+
17
+ # {.lists}' answer past {LIMIT}: the call's overloads cannot be read per value, and it answers `Dynamic[top]`.
18
+ OVER_LIMIT = :over_limit
19
+
20
+ module_function
21
+
22
+ # The lists, nil when no argument is a union, or {OVER_LIMIT}.
23
+ def lists(arg_types)
24
+ unions = arg_types.count { |arg| arg.is_a?(Type::Union) }
25
+ return nil if unions.zero?
26
+
27
+ if unions > 1
28
+ count = arg_types.reduce(1) { |product, arg| arg.is_a?(Type::Union) ? product * arg.members.size : product }
29
+ return OVER_LIMIT if count > LIMIT
30
+ end
31
+
32
+ arg_types.reduce([[]]) do |partials, arg|
33
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
34
+ partials.flat_map { |partial| members.map { |member| partial + [member] } }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -317,7 +317,7 @@ module Rigor
317
317
  end
318
318
 
319
319
  # Returns the receiver's known finite element count, or nil when the carrier does not pin a size.
320
- # Tuple and HashShape are pinned by construction; `Constant<…>` exposes the literal's `.size`. Other
320
+ # Tuple and HashShape are pinned by construction, and a `Constant` Range by its bounds. Other
321
321
  # shapes (Array[T], Range[T], Nominal) decline so the RBS tier widens.
322
322
  def finite_size(receiver)
323
323
  case receiver
@@ -192,8 +192,8 @@ module Rigor
192
192
 
193
193
  # #2115 — the numeric twin of `STRING_FOLD_BYTE_LIMIT`. A folded Integer (and each Integer part of a
194
194
  # folded Rational / Complex) is at most this many bits, the largest bit length whose decimal
195
- # rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds, `2 ** 13_606`
196
- # (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
195
+ # rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds,
196
+ # `2 ** 13_606` (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
197
197
  # rendering it in decimal — which describing, sorting and printing the `Constant` does — takes
198
198
  # minutes. `**` / `pow` / `<<` / `>>` also check their operands before invoking, so the big value
199
199
  # is never computed.
@@ -380,7 +380,7 @@ module Rigor
380
380
  # Normalises an input type into one of:
381
381
  # - `Array<Object>` for a `Constant` (1-element) or `Union[Constant…]` (n-element) — concrete values
382
382
  # to enumerate.
383
- # - `Type::IntegerRange` — bounded interval.
383
+ # - `Type::IntegerRange` / `Type::FloatRange` — bounded interval.
384
384
  # - `nil` — the input shape is not foldable.
385
385
  def numeric_set_of(type)
386
386
  case type
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "../rbs_type_translator"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module MethodDispatcher
9
+ module OverloadSelector
10
+ module_function
11
+
12
+ # Issue #2379 — every overload a supertype argument may select, when a nominal argument is wider than the
13
+ # parameter of the overload the call selected, `chosen`: `Integer#-` with a `Numeric` argument matched no
14
+ # overload and took the first, `(Integer) -> Integer`, although the runtime takes `(Float)`, `(Rational)` or
15
+ # `(Complex)` for those subclasses. Answers, in affinity order, every strictly typed overload whose every
16
+ # parameter takes its argument or is taken by a nominal one; nil when no argument is wider than `chosen`'s
17
+ # parameter or `chosen` is the only such overload.
18
+ # rubocop:disable-next Metrics/ParameterLists -- the selection inputs.
19
+ def contested_matches(method_definition, chosen, arg_types:, self_type:, instance_type:, type_vars:,
20
+ block_required:, environment:)
21
+ return nil unless arg_types.any?(Type::Nominal)
22
+
23
+ shared = { arg_types: arg_types, self_type: self_type, instance_type: instance_type,
24
+ type_vars: type_vars, block_required: block_required,
25
+ param_overrides: RbsExtended.param_type_override_map(method_definition, environment: environment),
26
+ environment: environment, keywords_last: false }
27
+ return nil unless wider_argument?(chosen, shared)
28
+
29
+ overloads = ReceiverAffinity.reorder(method_definition.method_types, self_type: self_type,
30
+ environment: environment)
31
+ matches = overloads.select do |method_type|
32
+ engages_block_shape?(method_type, block_required) &&
33
+ strictly_typed_params?(method_type, shared) && overlaps?(method_type, shared)
34
+ end
35
+ matches.empty? || (matches.size == 1 && matches.first.equal?(chosen)) ? nil : matches
36
+ end
37
+
38
+ class << self
39
+ private
40
+
41
+ # Whether a nominal argument is strictly wider than the parameter `method_type` binds it to: the parameter
42
+ # does not take the whole argument, and the argument takes the parameter (`Numeric` against `(Integer)`).
43
+ def wider_argument?(method_type, shared)
44
+ fun = method_type.type
45
+ arg_types = shared[:arg_types]
46
+ return false unless fun.respond_to?(:required_positionals) && arity_compatible?(fun, arg_types.size)
47
+
48
+ params = positional_params_for(fun, arg_types.size)
49
+ index = -1
50
+ params.any? do |param|
51
+ arg = arg_types[index += 1]
52
+ next false unless arg.is_a?(Type::Nominal)
53
+
54
+ param_type = translate_param(param, shared)
55
+ !param_type.accepts(arg, mode: :gradual).yes? && arg.accepts(param_type, mode: :gradual).yes?
56
+ end
57
+ end
58
+
59
+ # The strict pass's test with a nominal argument also counting where it takes the parameter: some value of
60
+ # a `Numeric` reaches `(Float)`.
61
+ def overlaps?(method_type, shared)
62
+ fun = method_type.type
63
+ ok = KeywordArguments.accepted_by?(fun, shared, true) { |*kv| accepts_param?(*kv, shared, true, true) }
64
+ return false unless ok
65
+
66
+ arg_types = shared[:arg_types]
67
+ return false unless arity_compatible?(fun, arg_types.size)
68
+
69
+ each_param_accepts?(positional_params_for(fun, arg_types.size), arg_types) do |param, arg|
70
+ accepts_param?(param, arg, shared, true) ||
71
+ (arg.is_a?(Type::Nominal) && arg.accepts(translate_param(param, shared), mode: :gradual).yes?)
72
+ end
73
+ end
74
+
75
+ # A parameter's type as the selector reads it: the `rigor:v1:param:` override, else the translated RBS type.
76
+ def translate_param(param, shared)
77
+ shared[:param_overrides][param.name] || RbsTypeTranslator.translate(
78
+ param.type,
79
+ self_type: shared[:self_type],
80
+ instance_type: shared[:instance_type],
81
+ type_vars: shared[:type_vars],
82
+ alias_expander: shared[:environment]&.rbs_loader
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -105,7 +105,7 @@ module Rigor
105
105
  # throws)
106
106
  # - `Hash[K, V]` / `HashShape{...}` → Tuple[K, V]
107
107
  # (Ruby yields `[key, value]` pairs as the element)
108
- # - `Constant<Array>` / `Constant<Range>` / `Constant<Set>`
108
+ # - `Constant<Array>` / `Constant<Range>`
109
109
  # → corresponding Constant element
110
110
  #
111
111
  # Receivers we cannot project (Top, Dynamic, unknown nominals, IO, …) decline so the RBS tier still
@@ -11,7 +11,7 @@ module Rigor
11
11
  # Kernel intrinsic shape-folding — precision tier for the `Kernel` module-functions whose return type
12
12
  # is a function of the argument's *shape*, not just its class.
13
13
  #
14
- # Today the only catalogued intrinsic is `Kernel#Array`. The default RBS sig is
14
+ # `Kernel#Array` is the shape-sensitive fold. Its default RBS sig is
15
15
  # `Array(untyped) -> Array[untyped]`, which collapses to `Array[Dynamic[top]]` for every caller. This
16
16
  # tier short-circuits with a precise answer when the argument's type lattice tells us what the result
17
17
  # element type MUST be:
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module MethodDispatcher
6
+ module OverloadSelector
7
+ module_function
8
+
9
+ # The overload a call answers when no selection pass matched it. A call with a block, on a method that
10
+ # declares a block overload, reads the block overloads (`block_call_fallback`, #2429) unless `block_fallback`
11
+ # is false. Otherwise prefer an overload that does not REQUIRE a block over `overloads.first`: methods like
12
+ # `Array#filter` / `Enumerable#map` declare the block-bearing overload first (`() { ... } -> Array[Elem]`)
13
+ # and the bare-call overload second (`() -> Enumerator[...]`), and a no-block `[1, 2].filter` yields an
14
+ # `Enumerator`.
15
+ def no_match_fallback(overloads, shared, block_fallback)
16
+ if block_fallback && shared[:block_required] && overloads.any? { |mt| overload_has_block?(mt) }
17
+ return block_call_fallback(overloads, shared)
18
+ end
19
+
20
+ [overloads.find { |mt| !overload_requires_block?(mt) } || overloads.first]
21
+ end
22
+
23
+ class << self
24
+ private
25
+
26
+ # Issue #2429 — the block-less arm's return is what the method answers without a block, so
27
+ # `URI.open(url, read_timeout: 3) { |io| }` read `StringIO | Tempfile` instead of the block's value. The
28
+ # answer is the block overload whose positional arity admits the call when only one does, or the one every
29
+ # such overload agrees on; otherwise none (`[]`, which the return path reads as `Dynamic[top]`).
30
+ # `[3, 1, 2].max_by(2.0) { }` takes `(Integer n) { } -> Array[Elem]`, not `() { } -> Elem?`, and overloads of
31
+ # differing returns that all admit the call are not decided by position.
32
+ def block_call_fallback(overloads, shared)
33
+ admitted = overloads.select { |mt| overload_has_block?(mt) && arity_admits?(mt, shared) }
34
+ return [] if admitted.empty?
35
+
36
+ admitted.map { |mt| mt.type.return_type }.uniq.size == 1 ? [admitted.first] : []
37
+ end
38
+
39
+ # The call's positional count (less a keyword hash the overload takes as keywords) fits `method_type`'s
40
+ # arity; a splat fits when some count of its elements does (#1801).
41
+ def arity_admits?(method_type, shared)
42
+ fun = method_type.type
43
+ arg_types = KeywordArguments.selection_positional(method_type, shared)
44
+ splats = shared[:splats]
45
+ return arity_compatible?(fun, arg_types.size) unless splats
46
+
47
+ lists = SplatArity.expansions(fun, arg_types, splats)
48
+ lists.nil? || lists.any? { |list| arity_compatible?(fun, list.size) }
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -4,9 +4,11 @@ require_relative "../../type"
4
4
  require_relative "../acceptance"
5
5
  require_relative "../rbs_type_translator"
6
6
  require_relative "alias_strict_nominals"
7
+ require_relative "contested_overloads"
7
8
  require_relative "facet_distribution"
8
9
  require_relative "imprecise_argument"
9
10
  require_relative "keyword_arguments"
11
+ require_relative "no_match_fallback"
10
12
  require_relative "proven_overload"
11
13
  require_relative "receiver_affinity"
12
14
  require_relative "splat_arity"
@@ -43,8 +45,10 @@ module Rigor
43
45
  # otherwise win purely by list position and inject false constants into the flow; they remain
44
46
  # selectable when the argument PROVES the value (strict pass) or when no other overload matches
45
47
  # (step 4's fallback picks the first overload regardless).
46
- # 4. If no overload matches at all, fall back to `method_types.first` so existing call sites keep
47
- # their phase 1 / 2b behavior. This preserves the fail-soft invariant of the dispatcher.
48
+ # 4. If no overload matches at all, fall back to the first overload that does not require a block (else
49
+ # `method_types.first`), so existing call sites keep their phase 1 / 2b behavior. This preserves the
50
+ # fail-soft invariant of the dispatcher. A call with a block reads the block overloads by arity instead
51
+ # (`no_match_fallback`, #2429).
48
52
  #
49
53
  # The selector is intentionally agnostic about the dispatch kind (instance vs singleton). Both kinds
50
54
  # share the same arity and acceptance shape; the difference is only in which `Definition` the caller
@@ -64,10 +68,10 @@ module Rigor
64
68
  # like `::Array[Elem]` substitute Elem before the accepts check, instead of degrading the param
65
69
  # to `Array[Dynamic[Top]]`.
66
70
  # @param block_required — when `true`, only overloads that declare a block clause are
67
- # considered (Slice 6 phase C sub-phase 1). The fallback also prefers a block-bearing overload
68
- # over `method_types.first`. When `false` (the Slice 4 phase 2c default) the selector behaves
69
- # exactly as before: `find` over arity-compatible overloads, falling back to the first
70
- # declaration.
71
+ # considered first (Slice 6 phase C sub-phase 1), and a call none of them matches is retried with
72
+ # the block ignored. Where nothing matches, a block call answers by arity among the block overloads
73
+ # (`block_call_fallback`, #2429), and otherwise the fallback prefers an overload that does not require a
74
+ # block over `method_types.first`.
71
75
  # @return the chosen overload, or nil when the definition has no method
72
76
  # types at all.
73
77
  def select(method_definition, **) = select_candidates(method_definition, member_wise: false, **).first
@@ -88,12 +92,14 @@ module Rigor
88
92
  # rubocop:disable-next Metrics/ParameterLists -- the selection keywords plus the member-wise switch.
89
93
  # @param keywords_last — the call's last argument is a keyword hash (`f(a, k: 1)`, not `f(a, { k: 1 })`).
90
94
  # @param splats — the indices of `arg_types` that are splat arguments (`f(*xs)`), or nil (#1801).
95
+ # @param block_fallback — false keeps a block call nothing matches on the block-less fallback (#2429): the
96
+ # block-parameter probe binds no parameters from an overload the call does not match.
91
97
  def select_candidates(definition, arg_types:, self_type:, instance_type:, type_vars: {},
92
98
  block_required: false, environment: nil, member_wise: true, keywords_last: false,
93
- splats: nil)
99
+ splats: nil, block_fallback: true)
94
100
  unless FacetDistribution.faceted?(arg_types)
95
101
  return select_declared(definition, arg_types, self_type, instance_type, type_vars, block_required,
96
- environment, false, keywords_last, splats)
102
+ environment, false, keywords_last, splats, block_fallback)
97
103
  end
98
104
 
99
105
  FacetDistribution.select(arg_types, definition, member_wise:, environment:) do |args, member|
@@ -101,7 +107,7 @@ module Rigor
101
107
  # gradually matches every arm and the strict pass's pick is the first by position, so the member-wise answer
102
108
  # is every gradual match (`:joined`), as the block probe reads it (#1750); the caller joins their returns.
103
109
  select_declared(definition, args, self_type, instance_type, type_vars, block_required, environment,
104
- member || (member_wise && :joined), keywords_last, splats)
110
+ member || (member_wise && :joined), keywords_last, splats, block_fallback)
105
111
  end
106
112
  end
107
113
 
@@ -114,7 +120,7 @@ module Rigor
114
120
  # first-overload fallback, as the return path's reading of the arguments as given always has (#1782).
115
121
  # rubocop:disable-next Metrics/ParameterLists -- the selection inputs plus the member-wise flag.
116
122
  def select_declared(method_definition, arg_types, self_type, instance_type, type_vars, block_required,
117
- environment, member, keywords_last, splats = nil)
123
+ environment, member, keywords_last, splats, block_fallback)
118
124
  declared = method_definition.method_types
119
125
  return [] if declared.empty?
120
126
 
@@ -159,12 +165,7 @@ module Rigor
159
165
  end
160
166
  return [] if member && member != :joined
161
167
 
162
- # No (usable) block at the call site: prefer an overload that does not REQUIRE a block over
163
- # `overloads.first`. Methods like `Array#filter` / `Enumerable#map` declare the block-bearing
164
- # overload first (`() { ... } -> Array[Elem]`) and the bare-call overload second
165
- # (`() -> Enumerator[...]`). Without this, a no-block `[1, 2].filter` would adopt the block
166
- # overload's `Array[Elem]` return when the call actually yields an `Enumerator`.
167
- [overloads.find { |mt| !overload_requires_block?(mt) } || overloads.first]
168
+ no_match_fallback(overloads, shared, block_fallback)
168
169
  end
169
170
 
170
171
  def overload_has_block?(method_type)
@@ -230,13 +231,25 @@ module Rigor
230
231
  # and the dispatch layer unions their returns (#521).
231
232
  matches = find_matching_overload(overloads, shared, strict: false)
232
233
  every_match = gradual || ImpreciseArgument.any_in?(shared[:arg_types], shared[:keywords_last])
233
- return matches.first(1) unless every_match
234
+ return [first_read_match(matches, shared)].compact unless every_match
234
235
 
235
236
  # Issue #1675 — and every overload the arguments' untyped parts reach on their own, after the
236
237
  # whole-argument matches so the singular `select` keeps its answer.
237
238
  ImpreciseArgument.with_stand_ins(matches, shared) { find_matching_overload(overloads, it, strict: false) }
238
239
  end
239
240
 
241
+ # Issue #2428 — the single winner of pass 2 is the first match that does not rest on a tuple's `maybe` for
242
+ # an `Array` of unknown arity, when there is one. Pass 1 declines a strict overload against a competitor
243
+ # whose parameter is an alias, an interface or `untyped`, and taking the tuple overload by list position
244
+ # then typed redis's `hmget(key, Array[String])` as `[String?]`, where its `(arg, Array[arg])` overload
245
+ # says `Array[String?]`. Only the tuple's `maybe` is re-asked; a record's `maybe` for an unread hash keeps
246
+ # its documented gradual-pass answer.
247
+ def first_read_match(matches, shared)
248
+ return matches.first if matches.size < 2 || !Acceptance.unread_array_in?(shared[:arg_types])
249
+
250
+ matches.find { |mt| Acceptance.refusing_unread_arrays { matches?(mt, shared) } } || matches.first
251
+ end
252
+
240
253
  # Pass 0. With every argument a plain value (a `Constant`, or a `Nominal` with no type arguments), the
241
254
  # first overload in declared order that the arguments do not rule out is the one RBS would take, and
242
255
  # the pass takes it only when that is certain: every parameter is a lone class naming its argument's
@@ -307,7 +320,10 @@ module Rigor
307
320
  next false unless engages_block_shape?(method_type, shared[:block_required])
308
321
 
309
322
  fun = method_type.type
310
- ok = KeywordArguments.accepted_by?(fun, shared, false) { |*kv| accepts_param?(*kv, shared, false, true) }
323
+ # Issue #2428 — a tuple keyword's `maybe` for an unread Array is no evidence here; pass 2 decides.
324
+ ok = Acceptance.refusing_unread_arrays do
325
+ KeywordArguments.accepted_by?(fun, shared, false) { |*kv| accepts_param?(*kv, shared, false, true) }
326
+ end
311
327
  next false unless ok
312
328
 
313
329
  arg_types = KeywordArguments.selection_positional(method_type, shared)
@@ -445,13 +461,7 @@ module Rigor
445
461
 
446
462
  # `shared` is the keyword bundle `select_candidates` assembled (see `find_matching_overload`).
447
463
  def accepts_param?(param, arg, shared, strict, keyword = false) # rubocop:disable Style/OptionalBooleanParameter
448
- param_type = shared[:param_overrides][param.name] || RbsTypeTranslator.translate(
449
- param.type,
450
- self_type: shared[:self_type],
451
- instance_type: shared[:instance_type],
452
- type_vars: shared[:type_vars],
453
- alias_expander: shared[:environment]&.rbs_loader
454
- )
464
+ param_type = translate_param(param, shared)
455
465
  # An `untyped` arg gradually accepts against every param, so a value-pinning param would be
456
466
  # "matched" with zero evidence and its value-precise return (`(nil) -> []`) would beat broader
457
467
  # overloads purely by list position. Decline the pair; only the strict pass (where the arg
@@ -467,8 +477,10 @@ module Rigor
467
477
  # A record's `maybe` for a `Hash` with a gradual arm or an open shape is no evidence for the overload:
468
478
  # with `({ a: Integer }) -> Integer | (Hash[Symbol, untyped]) -> String`, `{ **o, b: 2 }` has a key the
469
479
  # closed record forbids, yet the first overload won by list position. The gradual pass still takes it.
470
- # A `maybe` that survives with those records answering `no` has another source, and still counts.
471
- return false if strict && result.maybe? && Acceptance.maybe_rests_on_unread_hash?(param_type, arg)
480
+ # A tuple's `maybe` for an `Array` of unknown arity is the same: `([Integer, Integer]) -> Symbol |
481
+ # (Array[Integer]) -> String` answers `String` for an `Array[Integer]`. A `maybe` that survives with
482
+ # those shapes answering `no` has another source, and still counts.
483
+ return false if strict && result.maybe? && Acceptance.maybe_rests_on_unread_shape?(param_type, arg)
472
484
 
473
485
  result.yes? || result.maybe?
474
486
  end
@@ -20,7 +20,7 @@ module Rigor
20
20
  # (`Errno::EDOM`) and a non-literal argument decline to the RBS envelope. The other argument
21
21
  # shapes (`rand`, `rand(n)`, `rand(1.5)`) are deliberately NOT folded: the corpus uses them as its
22
22
  # "unknown Integer / Float" oracle, and their intervals belong to a change that replaces that
23
- # oracle first. The `Kernel` spelling is folded by {KernelDispatch#try_rand} behind ADR-91's
23
+ # oracle first. The `Kernel` spelling is folded by {KernelDispatch.try_dispatch} behind ADR-91's
24
24
  # ownership gate; this module owns `Singleton[Random]` and the shared interval reading.
25
25
  module RandomFolding
26
26
  module_function