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
@@ -8,8 +8,10 @@ require_relative "../rbs_type_translator"
8
8
  require_relative "../external_ancestor_resolution"
9
9
  require_relative "../void_origin"
10
10
  require_relative "../optimistic_origin"
11
+ require_relative "argument_distribution"
11
12
  require_relative "overload_selector"
12
13
  require_relative "self_substitute"
14
+ require_relative "../../run_scoped_slots"
13
15
 
14
16
  module Rigor
15
17
  module Inference
@@ -33,8 +35,8 @@ module Rigor
33
35
  # type-parameter names (`Array` -> `[:Elem]`, `Hash` -> `[:K, :V]`, ...) to build a substitution map;
34
36
  # that map is then threaded through {RbsTypeTranslator} so a return type like `::Array[Elem]`
35
37
  # resolves to `Nominal["Array", [Integer]]` rather than degrading the variable to `Dynamic[Top]`.
36
- # When arities mismatch (raw receiver, partial generics) the map is left empty and free variables
37
- # degrade as before.
38
+ # A raw receiver, or one with more arguments than the class declares, leaves the map empty and free
39
+ # variables degrade as before; fewer arguments bind the declared prefix (#1121).
38
40
  #
39
41
  # Slice 5 phase 1 projects shape-carrying receivers onto their underlying nominal so the existing
40
42
  # dispatch + substitution machinery works without duplication: `Tuple[Integer, String]` dispatches as
@@ -44,13 +46,14 @@ module Rigor
44
46
  #
45
47
  # Remaining limitations:
46
48
  #
47
- # * Keyword arguments reach `args` only as one trailing hash entry and are not matched against keyword
48
- # parameters, so overloads with required keywords are skipped.
49
- # * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
50
- # and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
49
+ # * Keyword arguments reach `args` as one trailing hash entry; the selector matches it against keyword
50
+ # parameters when the call's last argument is a keyword hash (#1727).
51
+ # * Method-level type parameters bind from the block return type (Slice 6 phase C) and from a
52
+ # positional parameter whose declared type is EXACTLY a type variable (issue #303 —
51
53
  # `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
52
- # such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
53
- # a rest positional (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
54
+ # such as `-> Array[T]`), plus a `Range[A]` element (#834). A variable reachable only through other
55
+ # container positions (`Array[T] arg`), a rest positional (`*T`), or a keyword parameter is still
56
+ # unbound and degrades to `Dynamic[Top]`.
54
57
  # A block-return variable that a parameter also names takes the value class the argument and the
55
58
  # block share once the call passes an argument, or stays unbound when they share none (see
56
59
  # {compose_block_type_vars}).
@@ -681,7 +684,7 @@ module Rigor
681
684
 
682
685
  # The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
683
686
  # on the discovery index's identity, and what a recording run may store in it.
684
- MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
687
+ MIXIN_ANCESTOR_MEMO_KEY = RunScopedSlots.register(:__rigor_mixin_ancestor_dispatch__)
685
688
  private_constant :MIXIN_ANCESTOR_MEMO_KEY
686
689
 
687
690
  def mixin_ancestor_memo(environment, scope)
@@ -855,7 +858,7 @@ module Rigor
855
858
  # the measurement. A `Scope` hands each analysed file its own discovery index, so an
856
859
  # identity-keyed store would pin every file's index, and every RBS definition resolved
857
860
  # against it, for the length of the run.
858
- CORE_STDLIB_ANCESTOR_MEMO_KEY = :__rigor_core_stdlib_ancestor_dispatch__
861
+ CORE_STDLIB_ANCESTOR_MEMO_KEY = RunScopedSlots.register(:__rigor_core_stdlib_ancestor_dispatch__)
859
862
  private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
860
863
 
861
864
  def core_stdlib_memo(environment, scope)
@@ -1086,7 +1089,42 @@ module Rigor
1086
1089
  block_required: !block_type.nil?,
1087
1090
  environment: environment
1088
1091
  )
1089
- return nil if candidates.empty?
1092
+ if candidates.empty?
1093
+ return unselected_return(method_definition, self_type, instance_type, type_vars, args, block_type,
1094
+ scope, call_node, call_site, environment, keywords_last)
1095
+ end
1096
+
1097
+ selected_return(candidates, method_definition, self_type, instance_type, type_vars, args, block_type, scope,
1098
+ call_node, call_site, environment, keywords_last)
1099
+ end
1100
+
1101
+ # Issue #2429 — a block call no overload matches, whose block overloads the arity fallback cannot decide
1102
+ # between, selects nothing. An argument union may still select per value (#2379): `S.h(flag ? "s" : 1) { }`
1103
+ # takes the `String` arm for `"s"` and the `Integer` arm for `1`. Otherwise the tier declines (nil), so a
1104
+ # later tier, such as the project's own definition of the method, answers.
1105
+ # rubocop:disable Metrics/ParameterLists
1106
+ def unselected_return(method_definition, self_type, instance_type, type_vars, args, block_type, scope,
1107
+ call_node, call_site, environment, keywords_last)
1108
+ # rubocop:enable Metrics/ParameterLists
1109
+ return nil if keywords_last
1110
+
1111
+ distributed = distributed_argument_return(method_definition, nil, self_type, instance_type, type_vars, args,
1112
+ block_type, scope, call_node, call_site, environment)
1113
+ distributed.equal?(NOT_DISTRIBUTED) ? nil : distributed
1114
+ end
1115
+
1116
+ # The return of the overloads the call selected: per argument value where a union or supertype argument
1117
+ # selects more than `candidates` ({#distributed_argument_return}), else their join.
1118
+ # rubocop:disable Metrics/ParameterLists
1119
+ def selected_return(candidates, method_definition, self_type, instance_type, type_vars, args, block_type,
1120
+ scope, call_node, call_site, environment, keywords_last)
1121
+ # rubocop:enable Metrics/ParameterLists
1122
+ if candidates.size == 1 && !keywords_last
1123
+ distributed = distributed_argument_return(method_definition, candidates.first, self_type, instance_type,
1124
+ type_vars, args, block_type, scope, call_node, call_site,
1125
+ environment)
1126
+ return distributed unless distributed.equal?(NOT_DISTRIBUTED)
1127
+ end
1090
1128
 
1091
1129
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
1092
1130
  join_candidate_returns(
@@ -1169,6 +1207,75 @@ module Rigor
1169
1207
  end
1170
1208
  end
1171
1209
 
1210
+ # Issue #2379 — a positional argument selects its overload per runtime value, as the receiver does
1211
+ # (`dispatch` distributes a union receiver). Selecting once for the whole argument took the first overload
1212
+ # it `maybe`-matched: `Integer#+` with an `Integer | Float` argument read `(Integer) -> Integer`, and with a
1213
+ # `Numeric` one as well. So a precise union argument selects per member ({ArgumentDistribution.lists}; a
1214
+ # member no overload takes keeps the first-overload fallback, as a non-matching argument does), and a
1215
+ # nominal argument wider than the selected overload's parameter takes every strictly typed overload it
1216
+ # overlaps ({OverloadSelector.contested_matches}). The returns join as a plain union, and a return that does
1217
+ # not translate declines the call (nil), as the #521 join does. Union arguments that combine past the list
1218
+ # limit answer `Dynamic[top]`: neither the first overload nor any narrower reading is sound there. When
1219
+ # every list selects `chosen` alone the call answers {NOT_DISTRIBUTED} and reads as before, so a precise
1220
+ # argument keeps its answer; so does a call holding an imprecise argument (the #521 join owns it) or a splat.
1221
+ # rubocop:disable Metrics/ParameterLists
1222
+ def distributed_argument_return(method_definition, chosen, self_type, instance_type, type_vars, args,
1223
+ block_type, scope, call_node, call_site, environment)
1224
+ # rubocop:enable Metrics/ParameterLists
1225
+ selector = { self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
1226
+ instance_type: instance_type, type_vars: type_vars, block_required: !block_type.nil?,
1227
+ environment: environment }
1228
+ return NOT_DISTRIBUTED unless distributable_arguments?(method_definition, args, call_node)
1229
+
1230
+ lists = ArgumentDistribution.lists(args)
1231
+ return Type::Combinator.untyped if lists.equal?(ArgumentDistribution::OVER_LIMIT)
1232
+
1233
+ per_list = argument_distribution_matches(method_definition, chosen, args, lists, selector)
1234
+ return NOT_DISTRIBUTED if per_list.nil?
1235
+
1236
+ overloads = per_list.flat_map(&:last).uniq
1237
+ if overloads.size == 1
1238
+ record_dispatch_provenance(method_definition, overloads.first, scope, call_node, call_site)
1239
+ end
1240
+ returns = per_list.flat_map do |arg_types, matches|
1241
+ matches.map do |method_type|
1242
+ join_candidate_returns(
1243
+ [method_type],
1244
+ method_definition: method_definition,
1245
+ self_type: self_type, instance_type: instance_type, type_vars: type_vars,
1246
+ args: arg_types, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
1247
+ alias_expander: environment.rbs_loader
1248
+ )
1249
+ end
1250
+ end
1251
+ returns.any?(&:nil?) ? nil : Type::Combinator.union(*returns.uniq)
1252
+ end
1253
+
1254
+ # Each argument list (`lists`, from {ArgumentDistribution.lists}, or the call's own) with the overloads it
1255
+ # selects, or nil when every list selects `chosen` alone. `selector` holds {OverloadSelector}'s keyword
1256
+ # inputs.
1257
+ def argument_distribution_matches(method_definition, chosen, args, lists, selector)
1258
+ return nil if lists.nil? && args.none?(Type::Nominal)
1259
+
1260
+ per_list = (lists || [args]).map do |arg_types|
1261
+ picked = chosen if arg_types.equal?(args)
1262
+ picked ||= OverloadSelector.select(method_definition, arg_types:, **selector)
1263
+ # #2429's block fallback may answer no overload; the call then reads as it did undistributed.
1264
+ return nil unless picked
1265
+
1266
+ contested = OverloadSelector.contested_matches(method_definition, picked, arg_types:, **selector)
1267
+ [arg_types, contested || [picked]]
1268
+ end
1269
+ per_list unless per_list.all? { |_, matches| matches.size == 1 && matches.first.equal?(chosen) }
1270
+ end
1271
+
1272
+ # Only a call with overloads to choose between and precise arguments it spells out one by one may select
1273
+ # differently per argument value: an imprecise argument is the #521 join's, a splat hides the positions.
1274
+ def distributable_arguments?(method_definition, args, call_node)
1275
+ method_definition.method_types.size > 1 && !args.empty? &&
1276
+ !ImpreciseArgument.any_in?(args, false) && !splat_indices(call_node, args)
1277
+ end
1278
+
1172
1279
  def join_distributed_returns(returns, keywords)
1173
1280
  return nil if returns.any?(&:nil?)
1174
1281
 
@@ -1412,11 +1519,24 @@ module Rigor
1412
1519
  # to {#shared_value_class} where both sides have one, and to `Dynamic[top]` otherwise. The key
1413
1520
  # stays in the map either way so {#compose_arg_type_vars} does not bind the variable from the
1414
1521
  # argument alone.
1522
+ #
1523
+ # A block return spelled as the variable beside falsey arms (`(nil | false | U)`, `U?`) solves the
1524
+ # variable from the block's type less those arms ({#strip_falsey_arms}): `filter_map` keeps only
1525
+ # the block's truthy values, so a block answering `String | nil` binds `U` to `String`. The strip
1526
+ # trusts the declared method to discard those values, so a receiver class whose Ruby source
1527
+ # defines the method itself leaves the variable unbound: `def filter_map(&b) = map(&b).compact`
1528
+ # under an `include Enumerable[Integer]` declaration keeps `false`, and `Array[true]` would fold
1529
+ # `b.last == false` always-falsey on correct code.
1415
1530
  def compose_block_type_vars(method_type, type_vars, block_type, args, scope:, call_node:, call_site:)
1416
1531
  return type_vars if block_type.nil?
1417
1532
 
1418
1533
  block_var_name = method_type_block_return_variable(method_type)
1419
1534
  return type_vars if block_var_name.nil?
1535
+
1536
+ arms = block_return_falsey_arms(method_type)
1537
+ return type_vars if arms.any? && project_redefined?(scope, call_site)
1538
+
1539
+ block_type = strip_falsey_arms(block_type, arms)
1420
1540
  return type_vars.merge(block_var_name => block_type) unless
1421
1541
  argument_reaches_variable?(method_type, block_var_name, args)
1422
1542
 
@@ -1751,14 +1871,78 @@ module Rigor
1751
1871
  end
1752
1872
 
1753
1873
  def block_return_variable(method_type)
1874
+ return_type = block_return_type(method_type)
1875
+ return return_type if return_type.is_a?(RBS::Types::Variable)
1876
+
1877
+ falsey_armed_variable(return_type)&.first
1878
+ end
1879
+
1880
+ def block_return_type(method_type)
1754
1881
  block = method_type.respond_to?(:block) ? method_type.block : nil
1755
1882
  return nil if block.nil?
1756
1883
 
1757
1884
  fun = block.type
1758
- return nil unless fun.respond_to?(:return_type)
1885
+ fun.respond_to?(:return_type) ? fun.return_type : nil
1886
+ end
1887
+
1888
+ # The falsey arms ({#falsey_armed_variable}) the block's declared return spells beside its variable;
1889
+ # empty for a bare variable.
1890
+ def block_return_falsey_arms(method_type)
1891
+ falsey_armed_variable(block_return_type(method_type))&.last || EMPTY_FALSEY_ARMS
1892
+ end
1893
+
1894
+ EMPTY_FALSEY_ARMS = [].freeze
1895
+ private_constant :EMPTY_FALSEY_ARMS
1896
+
1897
+ # A block return that is one type variable beside `nil` and/or `false` and nothing else —
1898
+ # `Enumerable#filter_map`'s `(nil | false | U)`, or `U?` — as `[variable, arms]`, the arms a subset
1899
+ # of `[:nil, :false]`. Anything else, `bool` among them, is nil: a block return must only lose
1900
+ # values the method itself discards, and a `false`-only arm has no carrier that removes `false`
1901
+ # alone from a `Dynamic` block type, so it declines too.
1902
+ def falsey_armed_variable(return_type)
1903
+ members =
1904
+ case return_type
1905
+ when RBS::Types::Optional then [return_type.type, RBS::Types::Bases::Nil.new(location: nil)]
1906
+ when RBS::Types::Union then return_type.types
1907
+ else return nil
1908
+ end
1909
+ variables, arms = members.partition { |member| member.is_a?(RBS::Types::Variable) }
1910
+ return nil unless variables.size == 1
1759
1911
 
1760
- return_type = fun.return_type
1761
- return_type.is_a?(RBS::Types::Variable) ? return_type : nil
1912
+ arm_names = arms.map { |arm| falsey_arm_name(arm) }
1913
+ return nil if arm_names.include?(nil) || !arm_names.include?(:nil)
1914
+
1915
+ [variables.first, arm_names.uniq]
1916
+ end
1917
+
1918
+ def falsey_arm_name(arm)
1919
+ case arm
1920
+ when RBS::Types::Bases::Nil then :nil
1921
+ when RBS::Types::Literal then arm.literal == false ? :false : nil # rubocop:disable Lint/BooleanSymbol
1922
+ end
1923
+ end
1924
+
1925
+ # The block's type less the falsey values the declared return spells beside the variable: its
1926
+ # truthy fragment for `nil | false`, its non-nil fragment for `nil` alone. A `Dynamic` block type
1927
+ # passes through unchanged, so an untyped block still binds `Dynamic[top]`. A block that answers
1928
+ # only the stripped values binds `bot`, so `filter_map { nil }` is `Array[bot]`: an Array that
1929
+ # holds no element.
1930
+ # Whether the receiver class's Ruby source defines the dispatched method, the probe
1931
+ # {#arg_binding_permitted?} also reads. Without a scope there is no discovery to consult.
1932
+ def project_redefined?(scope, call_site)
1933
+ return false if scope.nil?
1934
+
1935
+ class_name, method_name, kind = call_site
1936
+ return false if method_name.nil?
1937
+
1938
+ scope.discovered_method?(class_name, method_name, kind)
1939
+ end
1940
+
1941
+ def strip_falsey_arms(block_type, arms)
1942
+ return block_type if arms.empty?
1943
+ return Narrowing.narrow_truthy(block_type) if arms.include?(:false) # rubocop:disable Lint/BooleanSymbol
1944
+
1945
+ Narrowing.narrow_non_nil(block_type)
1762
1946
  end
1763
1947
 
1764
1948
  # ----- block parameter probe (Phase C sub-phase 1) -----
@@ -1862,7 +2046,9 @@ module Rigor
1862
2046
  instance_type: instance_type,
1863
2047
  type_vars: type_vars,
1864
2048
  block_required: true,
1865
- environment: environment
2049
+ environment: environment,
2050
+ # A call no overload matches binds no block parameter from the return path's block fallback (#2429).
2051
+ block_fallback: false
1866
2052
  )
1867
2053
  return [] unless method_type
1868
2054
 
@@ -1940,7 +2126,7 @@ module Rigor
1940
2126
  # rubocop:enable Metrics/ParameterLists
1941
2127
  unless FacetDistribution.faceted?(arg_types)
1942
2128
  return OverloadSelector.select_declared(method_definition, arg_types, selector_self, instance_type,
1943
- type_vars, block_required, environment, true, true, splats)
2129
+ type_vars, block_required, environment, true, true, splats, false)
1944
2130
  end
1945
2131
 
1946
2132
  facet_matches(method_definition, arg_types, selector_self, instance_type, type_vars, block_required,
@@ -1960,7 +2146,7 @@ module Rigor
1960
2146
  # The fallback list (`member` false) answers every gradual match rather than the strict pass's pick.
1961
2147
  OverloadSelector.select_declared(method_definition, list, selector_self, instance_type, type_vars,
1962
2148
  block_required, environment, member || :gradual, keywords_last,
1963
- splats)
2149
+ splats, false)
1964
2150
  end
1965
2151
  end
1966
2152
 
@@ -148,7 +148,7 @@ module Rigor
148
148
 
149
149
  # Runs the actual reduction, guarded by the rescue harness and the magnitude cap. Empty-collection
150
150
  # semantics match Ruby: `[].reduce(s, :op) == s` (seed form) and `[].reduce(:op) == nil` (no-seed
151
- # form → declines so the nominal fold's no-nil contract stands; see `fold_result`).
151
+ # form → declines so the nominal fold's no-nil contract stands; see the class comment).
152
152
  def execute_constant_reduce(members, operator, seed_value, seed_present)
153
153
  result =
154
154
  if seed_present
@@ -14,8 +14,8 @@ module Rigor
14
14
  # * `new(str)` / `new(str, opts)` — constructs a Regexp at fold time when the pattern argument is a
15
15
  # `Constant[String]`. The optional second argument may be a `Constant[Integer]` (flag bits), a
16
16
  # `Constant[true/false]` (IGNORECASE shorthand), or absent. Returns `Constant[Regexp]`.
17
- # * `compile(str)` / `compile(str, opts)` — `Regexp.compile` is a documented alias of `Regexp.new`
18
- # (same C entry point, `rb_reg_s_new`); shares `fold_new` verbatim.
17
+ # * `compile(str)` / `compile(str, opts)` — `Regexp.compile` is a documented alias of `Regexp.new`;
18
+ # shares `fold_new` verbatim.
19
19
  # * `union(*patterns)` / `union(array)` — builds the combined pattern at inference time when every
20
20
  # element is a `Constant[String]` or `Constant[Regexp]`. Delegates to the real `Regexp.union` so the
21
21
  # zero-arg (`/(?!)/`), single-array-argument, and pre-built-Regexp-argument forms all match Ruby's
@@ -67,8 +67,8 @@ module Rigor
67
67
  projected_self(class_name, receiver_args)
68
68
  end
69
69
 
70
- # Nil when every argument widens to the bare `Dynamic[top]`: `Hash[untyped, untyped]` says nothing
71
- # the raw `Hash` does not.
70
+ # Nil when every argument widens to untyped: a `Hash` of untyped keys and values says nothing the
71
+ # raw `Hash` does not.
72
72
  def projected_self(class_name, receiver_args)
73
73
  type_args = receiver_args.map { |arg| deep_widen(arg) }
74
74
  return nil if type_args.all? { |arg| untyped?(arg) }
@@ -13,17 +13,19 @@ module Rigor
13
13
  #
14
14
  # === Supported methods
15
15
  #
16
- # * `Set[]` — variadic constructor; each argument must be a `Constant[T]`. Returns `Constant[Set]`.
16
+ # * `Set[]` — variadic constructor. Returns `Constant[Set]` when every argument is a `Constant[T]`, and a
17
+ # typed `Set` otherwise.
17
18
  # * `Set.new` — zero-argument form; returns `Constant[Set.new]`.
18
- # * `Set.new(tuple)` — the argument must be a `Tuple` whose every element is a `Constant[T]`. Returns
19
- # `Constant[Set]`.
19
+ # * `Set.new(tuple)` — a `Tuple` argument. Returns `Constant[Set]` when every element is a `Constant[T]`,
20
+ # and a typed `Set` otherwise.
20
21
  #
21
22
  # === Non-constant / unsupported cases
22
23
  #
23
24
  # Returns `nil` (deferring to the next dispatcher tier) when:
24
25
  # - the receiver is not `Singleton[Set]`,
25
26
  # - the method is not `[]` or `new`,
26
- # - any argument is non-constant or structurally opaque.
27
+ # - the call is splatted or carries a block, or its `new` argument is not a `Tuple` or an `Array` / `Set`
28
+ # carrier.
27
29
  module SetFolding
28
30
  module_function
29
31
 
@@ -86,9 +88,9 @@ module Rigor
86
88
 
87
89
  # `Nominal[Set, [E]]`, `E` the members widened as an `Array.new` fill is (value pins dropped, a literal
88
90
  # container widened to its nominal) joined with a `Dynamic[top]` floor. The floor is the conservative half:
89
- # nothing widens a Set's element when the program mutates it (`add`, `<<`, `merge`, `replace`, `clear`
90
- # then `<<`, a callee that fills it), so the classes the set was built with are not a claim about what it
91
- # holds later, and a read of the element must not report on them (#2278).
91
+ # the mutation seams ({SetMutation}, #2294) see one store at a time, so the classes the set was built with
92
+ # are not a claim about what it holds later, and a read of the element must not report on them (#2278). The
93
+ # floor is also what lets those seams re-grow the set.
92
94
  def typed_set(types)
93
95
  return nil if types.empty?
94
96
 
@@ -194,6 +194,7 @@ module Rigor
194
194
  Type::Tuple => :dispatch_tuple,
195
195
  Type::HashShape => :dispatch_hash_shape,
196
196
  Type::Nominal => :dispatch_nominal_size,
197
+ Type::Constant => :dispatch_constant_set,
197
198
  Type::Difference => :dispatch_difference,
198
199
  Type::Refined => :dispatch_refined,
199
200
  Type::Intersection => :dispatch_intersection,
@@ -302,9 +303,38 @@ module Rigor
302
303
  when :flatten then array_nominal_flatten(nominal, args)
303
304
  when :compact then array_nominal_compact(nominal, args)
304
305
  end
306
+ when "Set"
307
+ set_nominal_algebra(nominal, method_name, args)
305
308
  end
306
309
  end
307
310
 
311
+ # The Set algebra that returns a new Set holding both operands' elements (issue #2294). RBS declares each
312
+ # as `(_Each[A]) -> Set[A]`, the receiver's element alone, so `ARGV.to_set | [1]` read `Set[String]` and a
313
+ # read of its `1` reported on correct code. The result is `Set[E | F]`, `F` the argument's elements with
314
+ # their value pins dropped ({SetMutation.element_types}).
315
+ SET_UNION_ALGEBRA = %i[| union + ^].to_set.freeze
316
+ private_constant :SET_UNION_ALGEBRA
317
+
318
+ def set_nominal_algebra(nominal, method_name, args)
319
+ return nil unless SET_UNION_ALGEBRA.include?(method_name) && args.size == 1
320
+
321
+ receiver = nominal.type_args.size == 1 ? nominal.type_args.first : Type::Combinator.untyped
322
+ added = Inference::SetMutation.element_types(args.first).map { |t| MethodDispatcher.array_new_element(t) }
323
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(receiver, *added)])
324
+ end
325
+
326
+ # The same algebra on a constant-folded Set whose argument is not constant (two constants fold upstream in
327
+ # {ConstantFolding}): the receiver's members and the argument's elements, typed as #2290 types a
328
+ # `Set[...]` literal over non-constant members ({SetFolding.typed_set}).
329
+ def dispatch_constant_set(constant, method_name, args)
330
+ return nil unless SET_UNION_ALGEBRA.include?(method_name) && args.size == 1
331
+ return nil unless Inference::SetMutation.constant?(constant)
332
+
333
+ members = constant.value.map { |v| Type::Combinator.constant_of(v) }
334
+ SetFolding.typed_set(members + Inference::SetMutation.element_types(args.first)) ||
335
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
336
+ end
337
+
308
338
  # `Integer#to_s` / `Integer#inspect` (#993) on a bare `Nominal[Integer]` receiver, ahead of the
309
339
  # existing `:*` binary fold. `#inspect` takes no argument and matches `#to_s()` byte for byte, so
310
340
  # it routes through the same no-arg branch.
@@ -317,7 +347,7 @@ module Rigor
317
347
  end
318
348
 
319
349
  # `decimal-int-string`'s predicate (`/\A-?\d+\z/`) admits the optional sign, so — unlike the
320
- # `IntegerRange#to_s` tier above, which declines a signed range because it cannot yet prove the
350
+ # `IntegerRange#to_s` tier below, which declines a signed range because it cannot yet prove the
321
351
  # sign is absent — a bare `Integer` (sign completely unknown) still reaches it for the base-10
322
352
  # case: base 10 is Ruby's default sign-and-digits grammar for every Integer. Any other base,
323
353
  # known or not statically known, drops to `non-empty-string`: `to_s(base)` never returns `""`,
@@ -422,14 +452,29 @@ module Rigor
422
452
  # Resolves the element type of a flattened `Array[element]`. Each `Array[U]` nesting level
423
453
  # contributes `U`; the per-level element types are unioned. `depth < 0` recurses without bound;
424
454
  # `depth == 0` stops (Ruby's `flatten(0)` is a no-op copy and returns the element unchanged).
455
+ #
456
+ # Issue #2427 — a `Tuple` element is an Array too and contributes the union of its own elements, each
457
+ # flattened one level deeper, and so does a `Tuple` member of a `Union` element. `ARGV.map { |x| [x] }`
458
+ # is `Array[[String]]`, and returning its `[String]` element unchanged made `.flatten.first.upcase`
459
+ # report `undefined method 'upcase' for [String]` on code Ruby runs.
460
+ #
461
+ # A nominal `Array[U]` member of a `Union` element is kept. RBS's `String#scan(Regexp)` answers
462
+ # `Array[String | Array[String?]]`, the `?` standing for an optional group, and flattening that member
463
+ # turned the idiomatic `str.scan(/k = (.+)/).flatten.map(&:strip)` into a `possible-nil-receiver` for a
464
+ # group that always matches.
425
465
  def flatten_nominal_element(element, depth)
426
466
  return element if depth.zero?
427
- return element unless array_nominal?(element)
428
467
 
429
- inner = element.type_args.first
430
- return element if inner.nil?
431
-
432
- flatten_nominal_element(inner, depth - 1)
468
+ case element
469
+ when Type::Tuple
470
+ Type::Combinator.union(*element.elements.map { |inner| flatten_nominal_element(inner, depth - 1) })
471
+ when Type::Union
472
+ Type::Combinator.union(*element.members.map do |member|
473
+ member.is_a?(Type::Tuple) ? flatten_nominal_element(member, depth) : member
474
+ end)
475
+ else
476
+ array_nominal?(element) ? flatten_nominal_element(element.type_args.first, depth - 1) : element
477
+ end
433
478
  end
434
479
 
435
480
  def array_nominal?(type)
@@ -472,7 +517,7 @@ module Rigor
472
517
  # leading sign. Base 10 is Ruby's decimal-literal grammar, so the result lifts to
473
518
  # `decimal-int-string`. Signed ranges fall through entirely (the result could carry a `-` sign
474
519
  # that `decimal-int-string` alone would need a sign proof for, matching the
475
- # `Nominal[Integer]` tier's own signed-base-10 carve-out below).
520
+ # `Nominal[Integer]` tier's own signed-base-10 carve-out above).
476
521
  #
477
522
  # #1004 — every other base (a different literal, such as 8 or 16, or a value that is not
478
523
  # statically known) does NOT lift to `octal-int-string` / `hex-int-string`: those refinements'
@@ -621,10 +666,10 @@ module Rigor
621
666
  Type::Combinator.positive_int
622
667
  end
623
668
 
624
- # Predicate-subset projections over a `Refined[base, predicate]` receiver. Today the catalogue is
625
- # the String case-normalisation pair: `s.downcase` over a `lowercase-string` receiver folds to
626
- # the same carrier (already lowercase), and `s.upcase` lifts a `lowercase-string` to
627
- # `uppercase-string`. Symmetric rules apply with the predicates swapped. Numeric-string
669
+ # Predicate-subset projections over a `Refined[base, predicate]` receiver. The catalogue is
670
+ # `REFINED_STRING_PROJECTIONS`, led by the String case-normalisation pair: `s.downcase` over a
671
+ # `lowercase-string` receiver folds to the same carrier (already lowercase), and `s.upcase` lifts a
672
+ # `lowercase-string` to `uppercase-string`. Symmetric rules apply with the predicates swapped. Numeric-string
628
673
  # idempotence over `#downcase` / `#upcase` is also recognised because a numeric string equals its
629
674
  # own case-normalisation.
630
675
  #
@@ -747,7 +792,7 @@ module Rigor
747
792
  # Compute the bounded-integer meet of two or more `IntegerRange` carriers. We compare via the
748
793
  # numeric `lower` / `upper` accessors (`-Float::INFINITY` / `Float::INFINITY` for the symbolic
749
794
  # ends), then map back to the symbolic-bound representation `IntegerRange.new` expects. The
750
- # disjoint-meet case cannot arise from sound member-wise projections in v0.0.4 but is guarded
795
+ # disjoint-meet case cannot arise from sound member-wise projections but is guarded
751
796
  # defensively to keep the carrier total.
752
797
  def narrow_integer_ranges(ranges)
753
798
  numeric_low = ranges.map(&:lower).max
@@ -1204,15 +1249,32 @@ module Rigor
1204
1249
  Type::Combinator.tuple_of(*tuple.elements, *other.elements)
1205
1250
  end
1206
1251
 
1207
- # `tuple.compact` — removes every element that is `Constant[nil]`. Folds only when every element
1208
- # is a `Constant` (so the nil set is decidable). Mixed-shape elements decline so the RBS tier
1209
- # widens.
1252
+ # `tuple.compact` — removes every element that is `Constant[nil]`. When every element is a
1253
+ # `Constant` the nil set is decidable and the result is the exact Tuple of the kept elements.
1254
+ # Otherwise an element that may be nil leaves the arity unknown, so the result is `Array[E]` with
1255
+ # `E` the elements' union minus its `nil` constituent (#2519) — never a fixed-arity Tuple. When
1256
+ # no element carries a `nil` constituent there is nothing to strip and the method declines so
1257
+ # the RBS tier answers (an element such as `Dynamic[top]` may still be nil at runtime, so the
1258
+ # Tuple is not kept either).
1210
1259
  def tuple_compact(tuple, _method_name, args)
1211
1260
  return nil unless args.empty?
1212
- return nil unless tuple.elements.all?(Type::Constant)
1213
1261
 
1214
- kept = tuple.elements.reject { |e| e.is_a?(Type::Constant) && e.value.nil? }
1215
- Type::Combinator.tuple_of(*kept)
1262
+ if tuple.elements.all?(Type::Constant)
1263
+ kept = tuple.elements.reject { |e| e.value.nil? }
1264
+ return Type::Combinator.tuple_of(*kept)
1265
+ end
1266
+
1267
+ tuple_compact_widened(tuple.elements)
1268
+ end
1269
+
1270
+ def tuple_compact_widened(elements)
1271
+ stripped = elements.map { |e| strip_nil_constituent(e) }
1272
+ return nil if stripped.zip(elements).all? { |s, e| s.equal?(e) }
1273
+
1274
+ element = Type::Combinator.union(*stripped)
1275
+ return Type::Combinator.tuple_of if element.is_a?(Type::Bot)
1276
+
1277
+ Type::Combinator.nominal_of("Array", type_args: [element])
1216
1278
  end
1217
1279
 
1218
1280
  # `uniq` (no block) → `Tuple` of the first occurrence of each distinct value. Folds only when
@@ -1293,7 +1355,8 @@ module Rigor
1293
1355
  end
1294
1356
 
1295
1357
  # `tuple.take(n)` — returns the first n elements as a new Tuple. The argument must be a
1296
- # `Constant[Integer]`. n <= 0 returns the empty Tuple; n >= size returns the full receiver.
1358
+ # `Constant[Integer]`. `n == 0` returns the empty Tuple, a negative `n` declines, and `n >= size`
1359
+ # returns the full receiver.
1297
1360
  def tuple_take(tuple, _method_name, args)
1298
1361
  n = non_negative_count_arg(args)
1299
1362
  return nil if n.nil?
@@ -1919,7 +1982,7 @@ module Rigor
1919
1982
  # `shape.values_at(:a, :b, ...)` with a list of static keys. Returns a `Tuple` whose per-position
1920
1983
  # values are the per-key value types (`Constant[nil]` for missing keys, mirroring Ruby's runtime
1921
1984
  # behaviour). A computed key takes the nil-bearing step {#hash_dig_step} gives it; the call falls
1922
- # through when any step does (a computed key on an open shape).
1985
+ # through when any step does (a computed key on an open or empty shape).
1923
1986
  def hash_values_at(shape, _method_name, args)
1924
1987
  return nil if args.empty?
1925
1988
 
@@ -20,7 +20,6 @@ module Rigor
20
20
  #
21
21
  # Centralising both here keeps the receiver-shape and constant-unwrap knowledge in one place: if
22
22
  # `Type::Singleton` or `Type::Constant` ever changes shape, there is a single edit rather than nine.
23
- # The per-tier fold bodies stay where they are — only the shared gate moves.
24
23
  module SingletonFolding
25
24
  module_function
26
25