rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -5,9 +5,11 @@ require_relative "../../type"
5
5
  require_relative "../../rbs_extended"
6
6
  require_relative "../range_constant"
7
7
  require_relative "../rbs_type_translator"
8
+ require_relative "../external_ancestor_resolution"
8
9
  require_relative "../void_origin"
9
10
  require_relative "../optimistic_origin"
10
11
  require_relative "overload_selector"
12
+ require_relative "self_substitute"
11
13
 
12
14
  module Rigor
13
15
  module Inference
@@ -42,14 +44,16 @@ module Rigor
42
44
  #
43
45
  # Remaining limitations:
44
46
  #
45
- # * `block_type:` is ignored; method types that constrain the block return type are not yet honored.
46
- # * Keyword arguments are not threaded through call_arg_types, so overloads with required keywords
47
- # are skipped (they cannot match the empty kwargs we send).
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.
48
49
  # * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
49
50
  # and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
50
51
  # `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
51
52
  # such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
52
53
  # a rest positional (`*T`), or a keyword parameter is still unbound and degrades to `Dynamic[Top]`.
54
+ # A block-return variable that a parameter also names takes the value class the argument and the
55
+ # block share once the call passes an argument, or stays unbound when they share none (see
56
+ # {compose_block_type_vars}).
53
57
  #
54
58
  # See docs/adr/4-type-inference-engine.md for the broader plan.
55
59
  # rubocop:disable-next Metrics/ModuleLength
@@ -65,6 +69,86 @@ module Rigor
65
69
  # objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
66
70
  ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
67
71
 
72
+ # Issue #1094 — the methods through which a value can answer Ruby's implicit `to_ary` conversion.
73
+ # Multiple assignment and block auto-splat convert through `rb_check_array_type`, which calls
74
+ # `to_ary` when defined and otherwise asks `respond_to_missing?`, dispatching to `method_missing`
75
+ # when that answers true — so a `Delegator` destructures its target without defining `to_ary`.
76
+ ARRAY_CONVERSION_HOOKS = %i[to_ary method_missing respond_to_missing?].freeze
77
+
78
+ # The owners whose declarations of those hooks are the defaults rather than an override:
79
+ # `BasicObject#method_missing` raises and `Kernel#respond_to_missing?` answers false.
80
+ ARRAY_CONVERSION_DEFAULT_OWNERS = %w[::BasicObject ::Object ::Kernel].freeze
81
+
82
+ # Core value classes whose instances never convert — the answer with no RBS environment to hand.
83
+ ARRAY_CONVERSION_FREE_CORE_CLASSES = %w[
84
+ Integer Float Symbol String Hash Range Regexp Proc NilClass TrueClass FalseClass
85
+ ].freeze
86
+
87
+ # The classes `Array` itself inherits from. A `Nominal[Object]` is routinely an array at runtime, so
88
+ # the RBS walk below, which reads the named class's own ancestry, cannot vouch for them.
89
+ ARRAY_SUPERCLASSES = %w[Object BasicObject].freeze
90
+
91
+ # Issue #1094 — whether an instance of `class_name` provably has no implicit array conversion, so
92
+ # `a, b = value` binds `a` to the value and `b` to `nil` rather than splatting it. Shares ADR-43's
93
+ # rationale for when an RBS ancestry is closed: the RBS of a class the environment KNOWS is the method
94
+ # set every other negative rule already trusts (`call.undefined-method` fires on it), while a class the
95
+ # environment does not know — a Ruby-source class, whatever it inherits — is an open hierarchy and
96
+ # declines, exactly as ADR-43 keeps it on the Dynamic fallback. Modules and `Array`'s own superclasses
97
+ # decline because a value of those types is routinely something else. A project `def` of any hook on
98
+ # the class, its source ancestors, or the default owners declines too: the project's source outranks
99
+ # the RBS it did not write. Without a `scope` only the core list answers.
100
+ def array_conversion_free?(class_name, scope)
101
+ name = class_name.to_s.delete_prefix("::")
102
+ return false if project_defines_array_conversion?(name, scope)
103
+ return true if ARRAY_CONVERSION_FREE_CORE_CLASSES.include?(name)
104
+ return false if scope.nil? || ARRAY_SUPERCLASSES.include?(name)
105
+
106
+ rbs_ancestry_array_conversion_free?(name, scope.environment)
107
+ end
108
+
109
+ # ADR-17's boundary for what the project's source can add to a class: a `def` in the analysed file
110
+ # (`Scope#discovered_method*`, walked through the source ancestry) or in a `pre_eval:` file
111
+ # (`Environment#project_patched_methods`). Both are asked about the class, its RBS ancestors and the
112
+ # default owners, because a hook added to any of them reaches the instance.
113
+ def project_defines_array_conversion?(name, scope)
114
+ return false if scope.nil?
115
+
116
+ owners = [name, *rbs_instance_ancestor_names(name, scope.environment),
117
+ *ARRAY_CONVERSION_DEFAULT_OWNERS.map { |owner| owner.delete_prefix("::") }].uniq
118
+ patched = scope.environment&.project_patched_methods
119
+ patched = nil if patched && patched.empty?
120
+ ARRAY_CONVERSION_HOOKS.any? do |hook|
121
+ scope.discovered_method_through_ancestors?(name, hook, :instance) ||
122
+ owners.any? do |owner|
123
+ scope.discovered_method?(owner, hook, :instance) ||
124
+ !patched&.lookup(class_name: owner, method_name: hook, kind: :instance).nil?
125
+ end
126
+ end
127
+ end
128
+
129
+ def rbs_instance_ancestor_names(name, environment)
130
+ return [] if environment.nil? || !Rigor::Reflection.rbs_class_known?(name, environment: environment)
131
+
132
+ definition = Rigor::Reflection.instance_definition(name, environment: environment)
133
+ return [] if definition.nil?
134
+
135
+ definition.ancestors.ancestors.map { |ancestor| ancestor.name.to_s.delete_prefix("::") }
136
+ end
137
+
138
+ def rbs_ancestry_array_conversion_free?(name, environment)
139
+ return false if environment.nil?
140
+ return false unless Rigor::Reflection.rbs_class_known?(name, environment: environment)
141
+ return false if environment.rbs_module?(name)
142
+
143
+ definition = Rigor::Reflection.instance_definition(name, environment: environment)
144
+ return false if definition.nil?
145
+
146
+ ARRAY_CONVERSION_HOOKS.none? do |hook|
147
+ method = definition.methods[hook]
148
+ method && !ARRAY_CONVERSION_DEFAULT_OWNERS.include?(method.defined_in.to_s)
149
+ end
150
+ end
151
+
68
152
  # Shared empty returns for the argument-position type-variable binding (issue #303). The
69
153
  # no-candidate answer is by far the common case — every non-generic overload takes it — so it must
70
154
  # not allocate.
@@ -74,6 +158,16 @@ module Rigor
74
158
  private_constant :EMPTY_TYPE_PARAM_NAMES
75
159
  NO_BINDING = [nil, nil].freeze
76
160
  private_constant :NO_BINDING
161
+ EMPTY_ARGUMENT_NODES = [].freeze
162
+ private_constant :EMPTY_ARGUMENT_NODES
163
+
164
+ # The classes {#shared_value_class} admits. Given an argument of the same class, each one's `+`
165
+ # answers that class for every receiver, subclass instances included (`String#+` answers a String;
166
+ # the numeric classes have no instances of a subclass). `class Name < String` is left out, since the
167
+ # `+` it inherits answers a plain String, and so is any class whose `coerce` may make `+` answer a
168
+ # third one.
169
+ CLOSED_VALUE_CLASSES = Set["Integer", "Float", "Rational", "Complex", "String"].freeze
170
+ private_constant :CLOSED_VALUE_CLASSES
77
171
 
78
172
  # Both spellings a resolved `RBS::Types::ClassInstance#name` — or a `Nominal#class_name` built
79
173
  # from one — may carry for `Range`; core signatures absolutise, but a plugin-contributed one
@@ -148,7 +242,8 @@ module Rigor
148
242
  receiver: context.receiver,
149
243
  method_name: context.method_name,
150
244
  args: context.args,
151
- environment: environment
245
+ environment: environment,
246
+ scope: context.scope
152
247
  )
153
248
  end
154
249
 
@@ -186,7 +281,8 @@ module Rigor
186
281
  return nil unless descriptor
187
282
 
188
283
  class_name, kind, receiver_args = descriptor
189
- method_definition = lookup_method(environment, class_name, kind, method_name, scope)
284
+ method_definition = lookup_method(environment, class_name, kind, method_name, scope,
285
+ call_node: call_node)
190
286
  return nil unless method_definition
191
287
  return nil if public_only && method_private?(method_definition)
192
288
  # Issue #823 — a declaration that states the member's presence and parameters but not its
@@ -209,7 +305,8 @@ module Rigor
209
305
  type_vars: type_vars,
210
306
  block_type: block_type,
211
307
  environment: environment,
212
- self_type_override: self_type_override,
308
+ self_type_override: self_type_override ||
309
+ SelfSubstitute.for(receiver, receiver_args, method_name, args, block_type),
213
310
  scope: scope,
214
311
  call_node: call_node
215
312
  )
@@ -325,11 +422,19 @@ module Rigor
325
422
  [Type::Combinator.union(*tuple.elements)]
326
423
  end
327
424
 
425
+ # An open shape's unseen keys may hold any value (rbs-erasure.md § *Open shapes with extra-value
426
+ # bounds*), so its projection carries a `Dynamic[top]` arm on both sides. Without it every read the
427
+ # shape tier does not answer — a `Union` receiver, a non-literal key — took the known values for a
428
+ # key outside them: `h = { a: 1 }; h.default = 0 if flag; h[:b] == 1` folded always-truthy.
328
429
  def hash_shape_type_args(shape)
329
430
  return [] if shape.pairs.empty?
330
431
 
331
432
  key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
332
433
  value_types = shape.pairs.values
434
+ if shape.open?
435
+ key_types += [Type::Combinator.untyped]
436
+ value_types += [Type::Combinator.untyped]
437
+ end
333
438
  [
334
439
  Type::Combinator.union(*key_types),
335
440
  Type::Combinator.union(*value_types)
@@ -344,20 +449,43 @@ module Rigor
344
449
  method_definition.accessibility == :private
345
450
  end
346
451
 
347
- def lookup_method(environment, class_name, kind, method_name, scope = nil)
452
+ def lookup_method(environment, class_name, kind, method_name, scope = nil, call_node: nil)
348
453
  direct = lookup_method_on(environment, class_name, kind, method_name)
349
454
  return direct if direct
350
455
 
456
+ # Issue #1173 — the include-edge sibling of the superclass bridges below: a discovered
457
+ # class's `include M` where M is RBS-known resolves M's declaration here. It runs BEFORE
458
+ # them because an include edge precedes the superclass edge in the MRO (`Sub < Hash` with
459
+ # `include M` chains `Sub → M → Hash`): when both a nearer mixin and a bridged ancestor
460
+ # declare the name, the mixin is the method that runs. See {#included_module_method}.
461
+ included = included_module_method(environment, class_name, kind, method_name, scope)
462
+ return included if included
463
+
351
464
  # ADR-43 — scoped inherited-method resolution. The direct lookup misses when `class_name` is a
352
465
  # Ruby-source subclass absent from RBS (so no ancestor walk runs). If its discovered
353
466
  # superclass chain reaches an allow-listed RBS-complete ancestor, resolve the method there so
354
467
  # inherited contract calls (`self.manifest` on a plugin) resolve and the normal call rules
355
468
  # apply. Bounded to the allow-list, so open hierarchies stay on the Dynamic fallback (no false
356
469
  # positive on `< ActionController::Base`).
357
- ancestor = allowed_rbs_complete_ancestor(environment, class_name, scope)
358
- return nil unless ancestor
470
+ ancestor = allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope)
471
+ return lookup_method_on(environment, ancestor, kind, method_name) if ancestor
472
+
473
+ # `extend M` in a class/module body lifts M's INSTANCE surface onto the extending object's
474
+ # singleton — `class F; extend T::Sig; sig { ... }; end` resolves `sig` through
475
+ # `T::Sig#sig`. Same contract as the superclass bridge: only allow-listed (manifest
476
+ # `rbs_complete_extends:`) modules qualify, so open hierarchies stay on Dynamic.
477
+ if kind == :singleton
478
+ mod = allowed_rbs_complete_extended_module(environment, class_name, method_name, scope,
479
+ call_node)
480
+ return lookup_method_on(environment, mod, :instance, method_name) if mod
481
+ end
359
482
 
360
- lookup_method_on(environment, ancestor, kind, method_name)
483
+ # Issue #527 slice 1 — the same shape, one RBS ancestry wider: a Ruby-source subclass of a
484
+ # CORE or STDLIB class (`class SubHash < Hash`, `< StandardError`, `< ::StringScanner`)
485
+ # resolves its inherited calls there. Injected HERE rather than as a new tier because
486
+ # `dispatch_one` keys `self`, `instance`, the type-variable map and `SelfSubstitute` on the
487
+ # RECEIVER's class name, so changing only the lookup gets the correct binding for free.
488
+ core_stdlib_ancestor_method(environment, class_name, kind, method_name, scope)
361
489
  end
362
490
 
363
491
  def lookup_method_on(environment, class_name, kind, method_name)
@@ -374,34 +502,491 @@ module Rigor
374
502
  # `class_name` is itself RBS-known (the direct lookup already had authority), or when the
375
503
  # discovered chain reaches no allow-listed class. The walk carries a visited set so a malformed
376
504
  # cyclic `A < B < A` source cannot loop.
377
- def allowed_rbs_complete_ancestor(environment, class_name, scope)
505
+ #
506
+ # ADR-43 WD4 — the allow-list's manifest-declared half: a loaded plugin may name its own
507
+ # contract classes in `rbs_complete_ancestors:` (e.g. rigor-graphql's `GraphQL::Schema::Object`),
508
+ # extending the engine's hard-coded seed without editing this constant.
509
+ def allowed_rbs_complete_ancestor(environment, class_name, kind, method_name, scope)
378
510
  return nil if scope.nil?
379
511
  return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
380
512
 
513
+ # A project `def` on the receiver class or on a nearer source ancestor shadows the
514
+ # bridged declaration. RBS dispatch runs before the discovered-method tier, so without
515
+ # this guard the bridge would resolve e.g. `field` on `GraphQL::Schema::Object` while
516
+ # the runtime actually calls a user `def self.field` on an intermediate `BaseObject` —
517
+ # a wrong return type and a false `undefined-method`/arity reading downstream.
518
+ return nil if scope.discovered_method?(class_name, method_name, kind)
519
+
520
+ registry = environment&.plugin_registry
521
+ each_source_ancestor_candidate(scope, class_name) do |candidate|
522
+ return nil if scope.discovered_method?(candidate, method_name, kind)
523
+ return candidate if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
524
+ registry&.rbs_complete_ancestor?(candidate)
525
+ end
526
+ nil
527
+ end
528
+
529
+ # Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
530
+ # STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
531
+ # `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
532
+ #
533
+ # Why this is not ADR-43's rejected alternative A. That ADR declined blanket inherited
534
+ # resolution because firing `call.undefined-method` against a PARTIAL gem RBS "would frighten
535
+ # working code". Two things narrow it here. The ancestry is core / stdlib, whose RBS is the
536
+ # method set every negative rule already trusts for a direct receiver of it. And the negative
537
+ # rules do not reach these receivers anyway: `undefined_method_diagnostic` and
538
+ # `arity_envelope_for` gate on `Reflection.rbs_class_known?` of the RECEIVER, which a
539
+ # Ruby-source subclass never is. So the risk this arm carries is not a new firing but a
540
+ # WRONG PRECISE TYPE propagating one hop — which is what the declines below are about.
541
+ #
542
+ # The declines, in order of what they protect:
543
+ #
544
+ # * `class_name` itself RBS-known — the direct lookup already had authority.
545
+ # * an ADR-26 plugin-declared open receiver, whose surface is larger than its declarations.
546
+ # * the subclass or a nearer SOURCE ancestor declares the name (ADR-110): the runtime calls
547
+ # the project's `def`, and resolving the inherited declaration would answer about a method
548
+ # that never runs. Asked of both discovery tables, because neither sees the whole of what a
549
+ # `def` / `attr_*` / `define_method` / `alias` contributes, and both suppress on budget
550
+ # exhaustion rather than answering "not declared" from an unfinished walk.
551
+ # * the walked ancestor, or the class the declaration is actually written on, is not core /
552
+ # stdlib. That is what keeps `class MyController < ActionController::Base` on `Dynamic[top]`
553
+ # (no RBS at all), and a subclass of an RBS-shipping GEM there too — slice 3's question.
554
+ # * an ADR-17 `pre_eval:` patch declares the name on the receiver or on any ancestor of the
555
+ # owner: the project has replaced the very method whose declaration this would adopt.
556
+ #
557
+ # Type variables are NOT inferred: a `Nominal[SubHash]` receiver carries no type arguments, so
558
+ # `build_type_vars` yields the empty map and `Hash[K, V]`'s free variables degrade to
559
+ # `Dynamic[top]` per the translator's contract. `SubHash#keys` is `Array[Dynamic[top]]`, which
560
+ # is exactly what a raw `Hash` receiver already answers — honest rather than a loss.
561
+ def core_stdlib_ancestor_method(environment, class_name, kind, method_name, scope)
562
+ return nil if scope.nil? || kind != :instance
563
+ return nil if environment.nil?
564
+
565
+ memo = core_stdlib_memo(environment, scope)
566
+ key = [class_name.to_s, method_name.to_sym]
567
+ return memo[key] if memo.key?(key)
568
+
569
+ store_unless_recorded(memo, key) do
570
+ compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
571
+ end
572
+ end
573
+
574
+ # Issue #1173 — the include-edge sibling of {#core_stdlib_ancestor_method}: a discovered
575
+ # class's `include M` where M is RBS-known (a project `sig/`, bundled core / stdlib, or a
576
+ # shipped gem signature) resolves M's declaration here. Ruby inserts an included module into
577
+ # the ancestor chain outright, so adopting its declaration is the dispatch the runtime
578
+ # performs — unlike the superclass arm there is no "which classes may be read as complete"
579
+ # question, only the usual shadow guards: a project `def` on the receiver or a nearer
580
+ # ancestor, an outside-the-body `include` / `class_eval` mark (#992), or an ADR-17 `pre_eval:`
581
+ # patch each mean the declaration found is not the method that runs.
582
+ #
583
+ # The walk reuses {ExternalAncestorResolution} with `mixins: true` — an include edge precedes
584
+ # the superclass edge at each BFS node, matching the MRO — and the answer is adopted ONLY
585
+ # when the resolved owner is an RBS module (`environment.rbs_module?`). A superclass-owned
586
+ # answer keeps declining: a non-core ancestor class is the gap #527's superclass slice
587
+ # deferred, and the walk's ordering already gave every include edge its chance first.
588
+ #
589
+ # `kind` is `:instance` only: an `include`d module's `def self.x` is not callable on the
590
+ # includer (the singleton side is a separate #527 item). `self` needs no help here —
591
+ # `dispatch_one` keys `self` / `instance` on the RECEIVER's class name, so a `-> self`
592
+ # module method answers the includer, matching CRuby.
593
+ def included_module_method(environment, class_name, kind, method_name, scope)
594
+ return nil if scope.nil? || kind != :instance
595
+ return nil if environment.nil?
596
+
597
+ memo = mixin_ancestor_memo(environment, scope)
598
+ key = [class_name.to_s, method_name.to_sym]
599
+ return memo[key] if memo.key?(key)
600
+
601
+ store_unless_recorded(memo, key) do
602
+ compute_included_module_method(environment, class_name, method_name, scope)
603
+ end
604
+ end
605
+
606
+ # The declines are conjunctive and ordered as {compute_core_stdlib_ancestor_method}'s are:
607
+ # the two walks that read the project's tables run LAST, only once an RBS declaration is
608
+ # actually in hand, because they read `Scope#superclass_of` / `#includes_of` and would file a
609
+ # file-granular ancestry edge for every call site otherwise.
610
+ def compute_included_module_method(environment, class_name, method_name, scope)
611
+ return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
612
+ return nil if environment.plugin_registry&.open_receiver?(class_name)
613
+
614
+ definition, owner = Inference::ExternalAncestorResolution.resolve(
615
+ class_name, method_name, :instance,
616
+ scope: scope, environment: environment, record_dependencies: false, mixins: true
617
+ )
618
+ return nil if definition.nil?
619
+ return nil unless environment.rbs_module?(owner)
620
+ return nil if returns_the_walked_ancestry?(definition, owner, environment)
621
+ return nil if dynamic_surface_through_ancestors?(scope, class_name)
622
+ return nil if project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
623
+ return nil if source_declares_through_ancestors?(scope, class_name, method_name)
624
+
625
+ definition
626
+ end
627
+
628
+ # The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
629
+ # on the discovery index's identity, and what a recording run may store in it.
630
+ MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
631
+ private_constant :MIXIN_ANCESTOR_MEMO_KEY
632
+
633
+ def mixin_ancestor_memo(environment, scope)
634
+ ancestor_memo_slot(MIXIN_ANCESTOR_MEMO_KEY, environment, scope)
635
+ end
636
+
637
+ # The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
638
+ # the project's tables run LAST, only once a core / stdlib declaration is actually in hand.
639
+ # Every unresolved call on a Ruby-source receiver reaches here (`Widget.new.price` on a plain
640
+ # project class), and those walks read `Scope#superclass_of` / `#includes_of`, which file an
641
+ # ADR-46 ancestry edge. Running them unconditionally turned every cross-class METHOD call into
642
+ # a file-granular ancestry dependency — coarser than the symbol edge ADR-46 slice 4 files, and
643
+ # pinned against by `dependency_recorder_spec`. Reached at all, the edge is genuine: this
644
+ # answer does depend on the project not declaring the name on that ancestry.
645
+ def compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
646
+ return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
647
+ return nil if environment.plugin_registry&.open_receiver?(class_name)
648
+
649
+ definition, owner = Inference::ExternalAncestorResolution.resolve(
650
+ class_name, method_name, :instance,
651
+ scope: scope, environment: environment, record_dependencies: false, mixins: false
652
+ )
653
+ return nil if definition.nil?
654
+ return nil unless core_or_stdlib_owned?(environment, owner, definition)
655
+ return nil if returns_the_walked_ancestry?(definition, owner, environment)
656
+ return nil if dynamic_surface_through_ancestors?(scope, class_name)
657
+ return nil if project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
658
+ return nil if source_declares_through_ancestors?(scope, class_name, method_name)
659
+
660
+ definition
661
+ end
662
+
663
+ # The blocker this slice was first written without. CRuby PRESERVES THE SUBCLASS where core /
664
+ # stdlib RBS names the base class: `SubHash#merge` returns a `SubHash`, `SubSet#flatten` a
665
+ # `SubSet`, `SubPathname#basename` a `SubPathname`, `SubDate#+` a `SubDate` — verified against
666
+ # the interpreter. Adopting the declaration answers `Nominal[Hash]`, and because `Hash` IS
667
+ # RBS-known the negative rules then read it as a CLOSED surface: `sub.merge({}).own_method`
668
+ # drew an `error`-severity `call.undefined-method` on working code. That is ADR-5's failure
669
+ # exactly, one hop downstream — the propagation this slice's own boundary section names.
670
+ #
671
+ # RBS cannot distinguish the two families. `String#upcase: () -> String` really does return a
672
+ # plain `String` for a `String` subclass (Ruby 3.0 changed that), while `Hash#merge: () ->
673
+ # Hash[K, V]` really does return the subclass, and the two declarations are the same shape.
674
+ # So the answer is DECLINE, which is master's `Dynamic[top]` and therefore provably cannot
675
+ # regress a corpus target — rather than substituting the receiver as if the declaration read
676
+ # `-> self`. That substitution would be right for `merge` and wrong for `upcase`, and its
677
+ # wrongness is not purely a false negative: a `Nominal[SubStr]` that is really a `String`
678
+ # narrows `is_a?` guards and can reach `clause.unreachable` on a branch the runtime takes.
679
+ #
680
+ # `-> self` and `-> instance` returns are NOT affected and keep their precision: those already
681
+ # resolve against the receiver (`SubHash#clear` → `SubHash`, `SubStr#force_encoding` →
682
+ # `SubStr`, `MyError#exception` → `MyError`), which is what CRuby does.
683
+ #
684
+ # The test looks for the owner ANYWHERE in the return type, type arguments included. A first
685
+ # draft unwrapped only the top level, unions and optionals, on the reasoning that a class named
686
+ # inside `Array[...]` describes the elements rather than the returned object — true, and beside
687
+ # the point, because the ELEMENTS are subclass instances too. `Pathname#children: () ->
688
+ # Array[Pathname]` hands back an array of `SubPath`s (likewise `entries`, `each_child`,
689
+ # `ascend`, `descend`, `find`; only `glob` yields a plain `Pathname`), so
690
+ # `sub.children.first.own_method` fired the same `call.undefined-method` one level down.
691
+ # `Date#step`, `Date#upto` and `Set#classify` are the same family and escaped only by the shape
692
+ # of their declarations.
693
+ #
694
+ # The cost of the deeper walk is in the direction WD7 already accepts: `SubStr#chars` →
695
+ # `Array[String]` now declines although CRuby really does yield plain `String`s. `keys`,
696
+ # `to_a` and `classify` are unaffected, their arguments being type variables or `self`.
697
+ #
698
+ # An `Alias` that expands to the owner is not followed; that is a known gap in the FN direction.
699
+ def returns_the_walked_ancestry?(definition, owner, environment)
700
+ names = [owner.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)].to_set
701
+ method_types = definition.respond_to?(:method_types) ? definition.method_types : nil
702
+ return false if method_types.nil?
703
+
704
+ method_types.any? do |method_type|
705
+ return_type = method_type.type.respond_to?(:return_type) ? method_type.type.return_type : nil
706
+ mentions_class?(return_type, names)
707
+ end
708
+ rescue StandardError
709
+ # A signature whose return type cannot be read is a gap, and a gap declines.
710
+ true
711
+ end
712
+
713
+ # Whether any of `names` appears as a class instance anywhere in `type`, descending through
714
+ # every child an RBS type exposes — union members, the inside of an optional, and type
715
+ # ARGUMENTS. See {returns_the_walked_ancestry?}.
716
+ def mentions_class?(type, names, depth = 0)
717
+ return false if type.nil? || depth > RETURN_TYPE_UNWRAP_DEPTH
718
+ return true if own_class_name_matches?(type, names)
719
+ return false unless type.respond_to?(:each_type)
720
+
721
+ type.each_type.any? { |child| mentions_class?(child, names, depth + 1) }
722
+ end
723
+
724
+ def own_class_name_matches?(type, names)
725
+ type.is_a?(::RBS::Types::ClassInstance) && names.include?(type.name.to_s.delete_prefix("::"))
726
+ end
727
+
728
+ # A guard against a pathological or cyclic signature, not a semantic limit: real return types
729
+ # nest a level or two (`Array[Pathname]`, `Hash[Symbol, Array[String]]`).
730
+ RETURN_TYPE_UNWRAP_DEPTH = 8
731
+ private_constant :RETURN_TYPE_UNWRAP_DEPTH
732
+
733
+ # Issue #992's surface mark: a `Klass.include(M)` / `.prepend(M)` / `class_eval` written
734
+ # OUTSIDE the class body, which `ScopeIndexer` records as `ENVELOPE_DYNAMIC_MARK` because it
735
+ # can add members the in-body walks never see. `class Extended < Hash; end` followed by
736
+ # `Extended.include(Ext)` where `Ext#empty?` returns `42` must not adopt `Hash#empty?`. Asked
737
+ # of the receiver and of every SOURCE ancestor between it and the owner, because a mark on an
738
+ # intermediate reaches the receiver just as well.
739
+ def dynamic_surface_through_ancestors?(scope, class_name)
740
+ return true if dynamic_surface?(scope, class_name)
741
+
742
+ each_source_ancestor_candidate(scope, class_name) do |candidate|
743
+ return true if dynamic_surface?(scope, candidate)
744
+ end
745
+ false
746
+ end
747
+
748
+ def dynamic_surface?(scope, class_name)
749
+ Scope::DiscoveryIndex.rewritten_surface?(scope.parameter_envelopes_of(class_name))
750
+ end
751
+
752
+ # ADR-110's precedence, asked of both tables the project's own members land in. Either one
753
+ # answering true is a decline; both suppress (answer true) when their shared
754
+ # `Scope::ANCESTOR_WALK_LIMIT` budget runs out, and record a `BudgetTrace` hit there.
755
+ def source_declares_through_ancestors?(scope, class_name, method_name)
756
+ return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
757
+
758
+ !scope.user_def_through_ancestors(class_name, method_name).first.nil?
759
+ end
760
+
761
+ # Both ends of the declaration have to be core / stdlib: the ancestor the walk asked (`Hash`,
762
+ # `StringScanner`) and the class the declaration is written on (`Exception` for
763
+ # `StandardError#message`, `Comparable` for a `clamp`). Either being a gem's or the project's
764
+ # own RBS is slice 3's question, not this one's.
765
+ def core_or_stdlib_owned?(environment, owner, definition)
766
+ loader = environment.rbs_loader
767
+ return false if loader.nil? || !loader.respond_to?(:core_or_stdlib_class?)
768
+ return false unless loader.core_or_stdlib_class?(owner)
769
+
770
+ declared_on = definition.respond_to?(:defined_in) ? definition.defined_in : nil
771
+ return false if declared_on.nil?
772
+
773
+ loader.core_or_stdlib_class?(declared_on.to_s)
774
+ end
775
+
776
+ # ADR-17 — a `pre_eval:` file that reopens the receiver, any SOURCE ancestor between it and the
777
+ # owner, or any RBS ancestor of the owner, and redefines the name. The declaration this arm
778
+ # would adopt is then not the method that runs. The source chain is the half the first draft
779
+ # missed: `class Middle < Hash; end; class Leaf < Middle; end` with a `pre_eval:`
780
+ # `class Middle; def key?(k) = 42; end` answered `bool` for a call that returns `42`.
781
+ def project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
782
+ patched = environment.project_patched_methods
783
+ return false if patched.nil? || patched.empty?
784
+
785
+ owners = [class_name.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)]
786
+ each_source_ancestor_candidate(scope, class_name) { |candidate| owners << candidate }
787
+ owners.any? do |name|
788
+ !patched.lookup(class_name: name, method_name: method_name, kind: :instance).nil?
789
+ end
790
+ end
791
+
792
+ # Memo for the whole decision. Every call site of a class asks the same `(class, method)`
793
+ # question, and the answer is a pure function of the frozen discovery index and the
794
+ # environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
795
+ # dependency edges stores only the answers whose computation recorded nothing
796
+ # ({#store_unless_recorded}): the shadow probes above read the project's method tables, and
797
+ # serving an answer that read them from the memo would swallow those edges for every later
798
+ # caller.
799
+ #
800
+ # ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
801
+ # the measurement. A `Scope` hands each analysed file its own discovery index, so an
802
+ # identity-keyed store would pin every file's index, and every RBS definition resolved
803
+ # against it, for the length of the run.
804
+ CORE_STDLIB_ANCESTOR_MEMO_KEY = :__rigor_core_stdlib_ancestor_dispatch__
805
+ private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
806
+
807
+ def core_stdlib_memo(environment, scope)
808
+ ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO_KEY, environment, scope)
809
+ end
810
+
811
+ # The slot behind both memos. Whether this thread is recording is part of its key, so a slot
812
+ # filled with every answer is never read by a recording caller, which may only reuse the
813
+ # answers {#store_unless_recorded} lets through.
814
+ def ancestor_memo_slot(memo_key, environment, scope)
815
+ discovery = scope.discovery
816
+ recording = Rigor::Analysis::DependencyRecorder.recording?
817
+ slot = Thread.current[memo_key]
818
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment) && slot[2] == recording
819
+ slot = [discovery, environment, recording, {}]
820
+ Thread.current[memo_key] = slot
821
+ end
822
+ slot[3]
823
+ end
824
+
825
+ # Computes an answer, stores it under `key` when it may be served again, and returns it.
826
+ # Outside recording every answer is stored. Under ADR-46 recording the block runs in an
827
+ # ADR-84 WD2 capture window, which sees every edge the block files for the current consumer
828
+ # (the reads it withholds are not among them). An answer whose window stayed empty is stored:
829
+ # computing it again would read the same frozen tables and file nothing again, so serving it
830
+ # from the memo records exactly what recomputing does. That is most of them, since both
831
+ # computations withhold their ancestry walk and return before any recorded probe unless an
832
+ # RBS declaration answered. Any other answer is recomputed on every call, which files its
833
+ # edges each time, as a recording run always did.
834
+ def store_unless_recorded(memo, key, &)
835
+ return memo[key] = yield unless Rigor::Analysis::DependencyRecorder.recording?
836
+
837
+ answer, read_set = Rigor::Analysis::DependencyRecorder.capture(&)
838
+ memo[key] = answer if read_set.reads.empty? && read_set.missing.empty?
839
+ answer
840
+ end
841
+
842
+ # BFS over the scope's as-written ancestry tables — include edges first, then the
843
+ # superclass, matching the MRO — yielding every ancestor-name candidate. The tables store
844
+ # names AS WRITTEN — `"::API::Base"`, bare `"Base"` — so each hop resolves through the
845
+ # nesting-aware `ancestor_name_candidates` rather than a raw lookup. Deliberately
846
+ # NOT `external_ancestor_name_candidates`: that walk records `ancestry_sources` edges via
847
+ # `record_class_dependency`, which would mislabel a dispatch lookup as an ancestry edge.
848
+ #
849
+ # Issue #1173 — the walk follows include edges too, because a shadow guard that reads only
850
+ # the superclass chain misses the nearer half of the ancestry: `class C < Base; include M;
851
+ # end` chains `C → M → Base`, and a `def`, an outside-the-body mark, or a `pre_eval:` patch
852
+ # on a source `M` all outrank `Base`'s declaration. A candidate that names a project class /
853
+ # module continues the walk ({Scope#known_user_class?} — a module that defines nothing and
854
+ # mixes nothing in still gates what the arm may adopt); an RBS-known or unresolved one is
855
+ # yielded for the caller's guards but its ancestry is the RBS side's business. The include
856
+ # table is read RAW (`scope.discovered_includes`), the same reason the superclass table is:
857
+ # `Scope#includes_of` files `record_class_dependency` on every read — a miss included —
858
+ # and this walk runs for every unresolved call on a project class (`Widget.new`), so the
859
+ # reader would turn each one into an ancestry edge ADR-46 slice 4 keeps at symbol
860
+ # granularity (`dependency_recorder_spec`).
861
+ def each_source_ancestor_candidate(scope, class_name)
862
+ supers = scope.discovered_superclasses
863
+ includes = scope.discovered_includes
864
+ queue = [class_name.to_s]
865
+ seen = {}
866
+ until queue.empty?
867
+ current = queue.shift
868
+ next if current.nil? || seen[current]
869
+
870
+ seen[current] = true
871
+ ((includes[current] || []) + [supers[current]].compact).each do |raw|
872
+ scope.ancestor_name_candidates(current, raw).each do |candidate|
873
+ yield candidate
874
+ queue << candidate if scope.known_user_class?(candidate)
875
+ end
876
+ end
877
+ end
878
+ end
879
+
880
+ # The extend-edge twin of `allowed_rbs_complete_ancestor` (manifest `rbs_complete_extends:`).
881
+ # `extend M` lifts M's INSTANCE surface onto the extending class object's singleton, so a
882
+ # singleton call on a Ruby-source class can resolve through a module the class — or one of
883
+ # its discovered superclasses — extends. Returns the first resolved candidate name that a
884
+ # loaded plugin allow-lists, or nil. Same guards as the superclass bridge, minus the
885
+ # RBS-known receiver exit: the direct lookup has already missed by the time this runs, and
886
+ # a class that is BOTH source-defined and RBS-known (`class F` in `sig/` plus `extend T::Sig`
887
+ # in the body) still carries the source edge — the runtime ancestry contains the module
888
+ # either way, so withholding the bridge would leave a real `sig` opaque. A nearer source
889
+ # `def self.x` still shadows any bridged module method.
890
+ def allowed_rbs_complete_extended_module(environment, class_name, method_name, scope,
891
+ call_node = nil)
892
+ return nil if scope.nil?
893
+
894
+ registry = environment&.plugin_registry
895
+ return nil if registry.nil?
896
+
381
897
  supers = scope.discovered_superclasses
898
+ extends = scope.discovered_extends
899
+ queue = [class_name.to_s]
382
900
  seen = {}
383
- current = supers[class_name.to_s]
384
- until current.nil? || seen[current]
385
- return current if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(current)
901
+ until queue.empty?
902
+ current = queue.shift
903
+ next if current.nil? || seen[current]
386
904
 
387
905
  seen[current] = true
388
- current = supers[current]
906
+ # The class's own `def self.x` sits ahead of every `extend` — once it has run.
907
+ # `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
908
+ # written AFTER this `sig {}` does not suppress the bridge.
909
+ # ADR-119: shadowing already withholds the bridge, so a possible def withholds too.
910
+ return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
911
+
912
+ resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
913
+ registry, method_name, call_node)
914
+ return nil if resolved.equal?(EXTEND_OWNER_STOP)
915
+ return resolved if resolved
916
+
917
+ raw = supers[current]
918
+ scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
389
919
  end
390
920
  nil
391
921
  end
392
922
 
923
+ # One walk hop of `allowed_rbs_complete_extended_module`. Each `extend` edge binds to the
924
+ # first resolution candidate that exists at runtime. If that owner DEFINES `method_name`:
925
+ # an allow-listed RBS module is the answer; a project class or a non-allow-listed RBS name
926
+ # owns the call, so the hop returns {EXTEND_OWNER_STOP} and the outer walk must not search
927
+ # later extends or superclasses (a nested `Outer::CustomSig` would otherwise fall through
928
+ # to `T::Sig` and type the call as `nil`). An owner that does not define the method yields
929
+ # to the next extended module — Ruby's singleton ancestry searches every extend in turn.
930
+ def rbs_complete_extended_module_for(current, extends, environment, scope, registry,
931
+ method_name, call_node)
932
+ each_extended_module_name(current, extends, environment) do |mod_name|
933
+ owner = scope.ancestor_name_candidates(current, mod_name).find do |candidate|
934
+ scope.known_user_class?(candidate) ||
935
+ Rigor::Reflection.rbs_class_known?(candidate, environment: environment)
936
+ end
937
+ next if owner.nil?
938
+ next unless extend_owner_defines?(owner, method_name, call_node, scope, environment)
939
+
940
+ project_owned = scope.known_user_class?(owner)
941
+ return owner if !project_owned && registry.rbs_complete_extends?(owner)
942
+
943
+ return EXTEND_OWNER_STOP
944
+ end
945
+ nil
946
+ end
947
+
948
+ # ADR-119: "defines" here stops the allow-list walk (a withholding answer), so a possible def stays
949
+ # a definer — treating it as absent would let a later extend bridge a call the def may own.
950
+ def extend_owner_defines?(owner, method_name, call_node, scope, environment)
951
+ return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
952
+
953
+ !lookup_method_on(environment, owner, :instance, method_name).nil?
954
+ end
955
+
956
+ # Sentinel: a nearer `extend` answers `method_name`, so the allow-list must not continue.
957
+ EXTEND_OWNER_STOP = :__rbs_complete_extends_stop__
958
+ private_constant :EXTEND_OWNER_STOP
959
+
960
+ # The module names `current` extends, source table first (`discovered_extends`, stored in
961
+ # singleton-ancestor search order — nearest edge first) then the RBS side
962
+ # (`singleton_extended_modules`, already qualified) — an RBS superclass like `T::Struct`
963
+ # declares `extend T::Props::ClassMethods` in signature, and a source subclass inherits it.
964
+ def each_extended_module_name(current, extends, environment, &)
965
+ (extends[current] || []).each(&)
966
+ (environment&.singleton_extended_modules(current) || []).each(&)
967
+ end
968
+
393
969
  # Slice 4 phase 2d substitution map. Zips the class's declared type-parameter names against the
394
- # receiver's `type_args`. Returns an empty hash when either side is empty or when arities
395
- # disagree -- in both cases free variables in the method's return type degrade to `Dynamic[Top]`
396
- # per the translator's contract.
970
+ # receiver's `type_args`. Returns an empty hash when either side is empty or when the receiver
971
+ # carries MORE arguments than the class declares -- in every such case free variables in the
972
+ # method's return type degrade to `Dynamic[Top]` per the translator's contract.
973
+ #
974
+ # Issue #1121 -- FEWER arguments than parameters is not a disagreement, it is the partial
975
+ # application RBS itself licenses for a class whose trailing parameters declare a default:
976
+ # `Enumerator::Lazy[out E, out R = void]` is spelled `Enumerator::Lazy[Elem]` by the very
977
+ # signature that hands one back (`Enumerable#lazy: () -> Enumerator::Lazy[Elem]`). Withholding
978
+ # the whole map there dropped the element binding on every lazy-enumerator receiver, so the
979
+ # block parameter of `lazy.map { |x| … }` was `Dynamic[top]` and the chain could not recover the
980
+ # element type. The supplied prefix MUST bind in declaration order and the omitted trailing names
981
+ # stay unbound (`Dynamic[top]`), exactly as any other free variable does.
397
982
  def build_type_vars(environment, class_name, receiver_args)
398
983
  return NO_TYPE_VARS if receiver_args.empty?
399
984
 
400
985
  param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
401
986
  return NO_TYPE_VARS if param_names.empty?
402
- return NO_TYPE_VARS if param_names.size != receiver_args.size
987
+ return NO_TYPE_VARS if receiver_args.size > param_names.size
403
988
 
404
- param_names.zip(receiver_args).to_h
989
+ param_names.first(receiver_args.size).zip(receiver_args).to_h
405
990
  end
406
991
 
407
992
  # The shared empty substitution map: most receivers carry no type arguments, and the translator
@@ -428,13 +1013,16 @@ module Rigor
428
1013
  end
429
1014
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
430
1015
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
431
- # fallback returns the caller's type, not Object.
1016
+ # fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
1017
+ # type-argument-bearing projection through it ({SelfSubstitute}, #1092).
432
1018
  self_type = self_type_override || resolved_self_type
433
1019
 
434
1020
  candidates = OverloadSelector.select_candidates(
435
1021
  method_definition,
436
1022
  arg_types: args,
437
- self_type: self_type,
1023
+ # A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
1024
+ # read the static facet, as they did before the substitute carried the wrapping.
1025
+ self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
438
1026
  instance_type: instance_type,
439
1027
  type_vars: type_vars,
440
1028
  block_required: !block_type.nil?,
@@ -446,6 +1034,7 @@ module Rigor
446
1034
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
447
1035
  join_candidate_returns(
448
1036
  candidates,
1037
+ method_definition: method_definition,
449
1038
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
450
1039
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
451
1040
  alias_expander: environment.rbs_loader
@@ -485,17 +1074,20 @@ module Rigor
485
1074
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
486
1075
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
487
1076
  # rubocop:disable-next Metrics/ParameterLists
488
- def join_candidate_returns(candidates, self_type:, instance_type:, type_vars:, args:, block_type:,
489
- scope:, call_node:, call_site:, alias_expander: nil)
1077
+ def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
1078
+ block_type:, scope:, call_node:, call_site:, alias_expander: nil)
490
1079
  returns = candidates.map do |method_type|
491
1080
  full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
492
- RbsTypeTranslator.translate(
1081
+ returned = RbsTypeTranslator.translate(
493
1082
  method_type.type.return_type,
494
1083
  self_type: self_type,
495
1084
  instance_type: instance_type,
496
1085
  type_vars: full_type_vars,
497
1086
  alias_expander: alias_expander
498
1087
  )
1088
+ next returned unless combining_overload?(method_definition, method_type, call_site)
1089
+
1090
+ class_level_sum(returned, method_type, args)
499
1091
  end
500
1092
  return returns.first if returns.size == 1
501
1093
  return nil if returns.any?(&:nil?)
@@ -507,11 +1099,100 @@ module Rigor
507
1099
  end
508
1100
 
509
1101
  def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
510
- vars = compose_block_type_vars(method_type, type_vars, block_type)
1102
+ vars = compose_block_type_vars(method_type, type_vars, block_type, args,
1103
+ scope: scope, call_node: call_node, call_site: call_site)
511
1104
  compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
512
1105
  call_site: call_site)
513
1106
  end
514
1107
 
1108
+ # Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
1109
+ # at class level ({#class_level_sum}). Every overload spells its return as the sides the method adds
1110
+ # together, as a union or as one variable that covers both: `() -> (E | Integer)`,
1111
+ # `[T] () { (E) -> T } -> (Integer | T)`, `[T] (?T) -> (E | T)` and `[U] (?U) { (E) -> U } -> U`.
1112
+ # A value is not closed under that addition, so the value-pinned bindings of `E` (from the
1113
+ # receiver), `T` (from the argument, issue #303) and the block's type do not describe the result:
1114
+ # `[1, 2].each.sum(0.0)` is `3.0`, which `0.0 | 1 | 2` misses. `Array#fetch: [T] (int, T default)
1115
+ # -> (E | T)` is spelled the same way but
1116
+ # returns the default object itself, so the signature alone cannot tell the two apart and the
1117
+ # declaring module does. A class that declares its own `sum` states its own contract and keeps it.
1118
+ def combining_overload?(method_definition, method_type, call_site)
1119
+ return false unless call_site[1] == :sum
1120
+
1121
+ type_def = OptimisticOrigin.matching_type_def(method_definition, method_type)
1122
+ !type_def.nil? && type_def.defined_in.to_s.delete_prefix("::") == "Enumerable"
1123
+ end
1124
+
1125
+ # Apart from the range shortcut ({#range_coerced_seed?}), CRuby's `enum_sum` adds each value to an
1126
+ # accumulator that starts at the seed. Between two of these classes `+` answers the later one
1127
+ # (`1 + 0.5` and `0.5 + 1` are Floats, `1 + 1r` is a Rational), so the accumulator's class only ever
1128
+ # moves up this order.
1129
+ SUM_PROMOTION_RANKS = { "Integer" => 0, "Rational" => 1, "Float" => 2, "Complex" => 3 }.freeze
1130
+ private_constant :SUM_PROMOTION_RANKS
1131
+
1132
+ # CRuby's seed when the call passes none.
1133
+ SUM_DEFAULT_SEED = ["Integer"].freeze
1134
+ private_constant :SUM_DEFAULT_SEED
1135
+
1136
+ # The seeds CRuby's integer-range shortcut adds to with plain `+` ({#range_coerced_seed?}).
1137
+ RANGE_ADDED_SEEDS = Set["Integer", "Float"].freeze
1138
+ private_constant :RANGE_ADDED_SEEDS
1139
+
1140
+ # The class-level reading of a `sum` overload's translated return, or `Dynamic[top]` when a member
1141
+ # widens to none of the value classes {#value_classes} admits. A class, not a value, is what the
1142
+ # addition keeps closed: `sum` over `0.0` and `1 | 2` is `3.0`, a Float, and over `1.5 | 2.5` is
1143
+ # `4.0`.
1144
+ #
1145
+ # The union of the members' classes would still read wider than the runtime, because the seed
1146
+ # promotes every value it absorbs: `ints.each.sum(0.0)` is always a Float, and `Float | Integer`
1147
+ # fires `def.return-type-mismatch` against a declared `-> Float` on correct code. So each class is
1148
+ # taken as the class the accumulator reaches from each seed class ({#promoted_class}), and the seed
1149
+ # itself stays for a receiver that yields nothing: `ints.each.sum(0.0)` reads `Float`, and
1150
+ # `floats.each.sum(0)` reads `Float | Integer`, whose Integer is the empty receiver's `0`.
1151
+ def class_level_sum(type, method_type, args)
1152
+ return nil if type.nil?
1153
+
1154
+ members = value_classes(type)
1155
+ return Type::Combinator.untyped if members.nil?
1156
+
1157
+ seeds = sum_seed_classes(method_type, args)
1158
+ return Type::Combinator.untyped if seeds.nil? || range_coerced_seed?(method_type, seeds)
1159
+
1160
+ reached = seeds.flat_map { |seed| members.map { |member| promoted_class(seed, member) } }
1161
+ Type::Combinator.union(*(seeds | reached).map { |name| Type::Combinator.nominal_of(name) })
1162
+ end
1163
+
1164
+ # Whether CRuby may skip the accumulator and read the seed as a Float. With no block and a seed that
1165
+ # is not a Float, `enum_sum` sums a range with Integer endpoints by Gauss's formula and adds the
1166
+ # result to the seed. An Integer seed takes plain `+`; any other goes through `Integer#coerce`, which
1167
+ # converts it with `Float()`: `(1..3).sum(0r)` is `6.0` and `(1..3).sum("1.5")` is `7.5`. Any object
1168
+ # that answers `begin`, `end` and `exclude_end?` takes the same path, so the receiver's class cannot
1169
+ # rule it out.
1170
+ def range_coerced_seed?(method_type, seeds)
1171
+ method_type.block.nil? && seeds.any? { |seed| !RANGE_ADDED_SEEDS.include?(seed) }
1172
+ end
1173
+
1174
+ # The classes of the value `sum` starts from: the argument when the overload takes one and the call
1175
+ # passes it, and CRuby's `0` otherwise.
1176
+ def sum_seed_classes(method_type, args)
1177
+ fun = method_type.type
1178
+ return SUM_DEFAULT_SEED if args.empty? || !fun.respond_to?(:required_positionals)
1179
+ return SUM_DEFAULT_SEED if fun.required_positionals.empty? && fun.optional_positionals.empty?
1180
+
1181
+ value_classes(args.first)
1182
+ end
1183
+
1184
+ # The class the accumulator reaches when a `member` value is added to a `seed`-class one. `+` does not
1185
+ # add a String and a number (`"" + 1` and `1 + ""` raise), so such a pair keeps the member's class,
1186
+ # which reads wider than the runtime rather than narrower. The range shortcut that does convert a
1187
+ # String seed never reaches here ({#range_coerced_seed?}).
1188
+ def promoted_class(seed, member)
1189
+ seed_rank = SUM_PROMOTION_RANKS[seed]
1190
+ member_rank = SUM_PROMOTION_RANKS[member]
1191
+ return member if seed_rank.nil? || member_rank.nil?
1192
+
1193
+ seed_rank > member_rank ? seed : member
1194
+ end
1195
+
515
1196
  # Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
516
1197
  # `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
517
1198
  # kind]` triple the {VoidOrigin} carries.
@@ -558,13 +1239,160 @@ module Rigor
558
1239
  # block return type at the same call site. Anything outside this exact shape (no block clause,
559
1240
  # an `untyped` block, a non-variable block return type, a variable not declared in
560
1241
  # `type_params`) returns the original `type_vars` so fallbacks stay consistent.
561
- def compose_block_type_vars(method_type, type_vars, block_type)
1242
+ #
1243
+ # The block alone does not decide a variable that a parameter's type also names once the call
1244
+ # passes an argument. `Enumerable#sum: [U] (?U) { (E) -> U } -> U` adds the block's values to the
1245
+ # argument, `Enumerable#inject: [A] (A initial) { (A, E) -> A } -> A` returns the argument for an
1246
+ # empty receiver, and `Hash#transform_keys: [K2] (hash[_Key, K2]) { (K) -> K2 } -> Hash[K2, V]`
1247
+ # takes a mapping hit's value without yielding. `Dynamic[block_type]` would not do: a `Dynamic`
1248
+ # receiver dispatches through its static facet and answers exactly, so a facet that misses the
1249
+ # runtime value is wrong one call later (`(h.sum(0.0) { |_k, v| v } / h.size).nan?` read
1250
+ # `Integer#nan?`). Nor would joining in the argument as it stands, since
1251
+ # `[1, 2].each.sum(0.0) { |x| x }` is `3.0`, which neither side contains. The variable is bound
1252
+ # to {#shared_value_class} where both sides have one, and to `Dynamic[top]` otherwise. The key
1253
+ # stays in the map either way so {#compose_arg_type_vars} does not bind the variable from the
1254
+ # argument alone.
1255
+ def compose_block_type_vars(method_type, type_vars, block_type, args, scope:, call_node:, call_site:)
562
1256
  return type_vars if block_type.nil?
563
1257
 
564
1258
  block_var_name = method_type_block_return_variable(method_type)
565
1259
  return type_vars if block_var_name.nil?
1260
+ return type_vars.merge(block_var_name => block_type) unless
1261
+ argument_reaches_variable?(method_type, block_var_name, args)
1262
+
1263
+ shared = shared_value_class(method_type, block_var_name, args, block_type, call_node)
1264
+ bound = shared && arg_binding_permitted?(scope, call_node, call_site) ? shared : Type::Combinator.untyped
1265
+ type_vars.merge(block_var_name => bound)
1266
+ end
1267
+
1268
+ # Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
1269
+ # inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
1270
+ # parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
1271
+ # arrive as one more entry in `args`, so any argument counts as reaching every parameter.
1272
+ def argument_reaches_variable?(method_type, name, args)
1273
+ return false if args.empty?
1274
+
1275
+ method_type.type.each_param.any? { |param| mentions_variable?(param.type, name) }
1276
+ end
1277
+
1278
+ # The one value class that the arguments landing in `name`'s parameters and the block's type all
1279
+ # share, or nil to leave the variable `Dynamic[top]`. A class, not a value, because a combining
1280
+ # method keeps a class closed and not a value: `[1, 2].each.sum(1) { |x| -x }` is `-2`, which
1281
+ # neither `1` nor `-1 | -2` contains. RBS reads `[U] (?U) { (E) -> U } -> U` the same way, as a
1282
+ # `U` that covers the argument and the block alike. One class and not a union of several, because
1283
+ # `sum` absorbs: over Integers, `sum(0.0)` answers a Float every time, and `Float | Integer` would
1284
+ # fire `def.return-type-mismatch` against a declared `-> Float`.
1285
+ #
1286
+ # The block's type is one typing of its body, so it covers every call only when nothing the block
1287
+ # receives depends on the variable ({#block_receives_variable?}). The argument binding's gate
1288
+ # applies ({#arg_binding_permitted?}), since this reads the argument. The positions must be static
1289
+ # ({#arguments_at_variable}), and every side must widen to a class ({#value_classes}). The block's
1290
+ # type is trusted as far as the engine trusts it, so a hash filled through an alias, which reads
1291
+ # narrower than it is, binds its narrower class here too.
1292
+ def shared_value_class(method_type, name, args, block_type, call_node)
1293
+ return nil if block_receives_variable?(method_type.block, name)
1294
+
1295
+ reaching = arguments_at_variable(method_type, name, args, call_node)
1296
+ return nil if reaching.nil?
1297
+
1298
+ sides = [*reaching, block_type].map { |type| value_classes(type) }
1299
+ return nil unless sides.all?
1300
+
1301
+ classes = sides.flatten(1).uniq
1302
+ classes.size == 1 ? Type::Combinator.nominal_of(classes.first) : nil
1303
+ end
1304
+
1305
+ # Whether the block's parameters or its `self` name the variable, as `inject`'s accumulator
1306
+ # (`{ (A, E) -> A }`) and `produce`'s previous element (`{ (T prev) -> T }`) do. Such a parameter
1307
+ # holds the argument on the first call and the block's own result after it, and the pass that
1308
+ # typed the block typed it once, as whatever reached it: the RBS probe leaves it untyped, but
1309
+ # `IteratorDispatch` hands an Array receiver's `inject` the seed, and a `&:+` block then reads
1310
+ # `0.+`, so `[1.5, 2].inject(0, &:+)`, which is `3.5`, would bind `Integer`.
1311
+ def block_receives_variable?(block, name)
1312
+ return true if block.self_type && mentions_variable?(block.self_type, name)
1313
+
1314
+ block.type.each_param.any? { |param| mentions_variable?(param.type, name) }
1315
+ end
1316
+
1317
+ # The arguments that land in a positional parameter whose whole type is `name`, or nil when the
1318
+ # call's positions or the signature's shape leave that open. The call must pass plain positional
1319
+ # arguments: after a `*splat` that turns out empty, the next argument lands one parameter earlier,
1320
+ # and a forwarded `...` or keyword arguments may land anywhere. The signature must name `name` only
1321
+ # as a whole leading positional parameter's type, and have no trailing positional parameter at
1322
+ # all. One that names the variable takes its argument from the end of the list, which pairing the
1323
+ # leading parameters with the arguments in order would miss; one that does not is declined too,
1324
+ # conservatively.
1325
+ def arguments_at_variable(method_type, name, args, call_node)
1326
+ return nil unless plain_positional_arguments?(call_node, args.size)
1327
+
1328
+ fun = method_type.type
1329
+ return nil unless fun.respond_to?(:trailing_positionals) && fun.trailing_positionals.empty?
1330
+ return nil if named_outside_positionals?(fun, name)
1331
+
1332
+ positionals = fun.required_positionals + fun.optional_positionals
1333
+ positionals.zip(args).each_with_object([]) do |(param, arg), reaching|
1334
+ type = param.type
1335
+ next unless mentions_variable?(type, name)
1336
+ return nil unless type.is_a?(RBS::Types::Variable)
1337
+
1338
+ reaching << arg unless arg.nil?
1339
+ end
1340
+ end
1341
+
1342
+ def plain_positional_arguments?(call_node, count)
1343
+ return false unless call_node.is_a?(Prism::CallNode)
1344
+
1345
+ arguments = call_node.arguments&.arguments || EMPTY_ARGUMENT_NODES
1346
+ arguments.size == count && arguments.none? do |argument|
1347
+ case argument
1348
+ when Prism::SplatNode, Prism::ForwardingArgumentsNode, Prism::KeywordHashNode then true
1349
+ else false
1350
+ end
1351
+ end
1352
+ end
1353
+
1354
+ def named_outside_positionals?(fun, name)
1355
+ others = [fun.rest_positionals, fun.rest_keywords].compact +
1356
+ fun.required_keywords.values + fun.optional_keywords.values
1357
+ others.any? { |param| mentions_variable?(param.type, name) }
1358
+ end
1359
+
1360
+ # The class names `type`'s union members widen to, or nil when a member widens to none of
1361
+ # {CLOSED_VALUE_CLASSES}.
1362
+ def value_classes(type)
1363
+ members = type.is_a?(Type::Union) ? type.members : [type]
1364
+ classes = members.map { |member| value_class(member) }
1365
+ classes.all? ? classes : nil
1366
+ end
1367
+
1368
+ # A literal widens to its value's class, a bounded number to `Integer` or `Float`, and a refinement
1369
+ # or a difference to its base's class. Everything else declines: a generic class (`[] + [:a]`
1370
+ # concatenates elements rather than keeping either side's), a carrier with no class (a tuple, a
1371
+ # hash shape, `Dynamic`), and `nil` or `NilClass`, whose arm would fire
1372
+ # `call.possible-nil-receiver` where `Array#max` and `#first` bet on a non-empty receiver.
1373
+ def value_class(type)
1374
+ case type
1375
+ when Type::Nominal then closed_value_class(type.class_name)
1376
+ when Type::Constant then closed_value_class(type.value.class.name)
1377
+ when Type::IntegerRange then "Integer"
1378
+ when Type::FloatRange then "Float"
1379
+ when Type::Refined, Type::Difference then value_class(type.base)
1380
+ end
1381
+ end
1382
+
1383
+ def closed_value_class(class_name)
1384
+ name = class_name&.delete_prefix("::")
1385
+ CLOSED_VALUE_CLASSES.include?(name) ? name : nil
1386
+ end
566
1387
 
567
- type_vars.merge(block_var_name => block_type)
1388
+ # A signature nested past {RETURN_TYPE_UNWRAP_DEPTH} counts as naming the variable, which is the
1389
+ # untyped answer.
1390
+ def mentions_variable?(type, name, depth = 0)
1391
+ return true if depth > RETURN_TYPE_UNWRAP_DEPTH
1392
+ return type.name == name if type.is_a?(::RBS::Types::Variable)
1393
+ return false unless type.respond_to?(:each_type)
1394
+
1395
+ type.each_type.any? { |child| mentions_variable?(child, name, depth + 1) }
568
1396
  end
569
1397
 
570
1398
  # Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
@@ -607,10 +1435,25 @@ module Rigor
607
1435
  name, bound = param_binding(param, arg, declared, type_vars)
608
1436
  next if name.nil?
609
1437
 
1438
+ bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
610
1439
  bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
611
1440
  end
612
1441
  end
613
1442
 
1443
+ # Issue #1347 — a variable that declares an upper bound (`[T < X]`) binds the argument widened off its
1444
+ # value-pinned members. The bound constrains a class, and `Rational#*: [T < Numeric](T) -> T` returns a
1445
+ # value of the argument's class, not the argument, so `r * 0.5` read the literal `0.5`. An unbounded
1446
+ # variable keeps the literal (`Ractor.make_shareable("x")` is `"x"`); a bounded identity return
1447
+ # (`String#setbyte`) gives its literal up. Any bound counts: `upper_bound` answers only a class, singleton
1448
+ # or interface bound, so an alias, union, intersection or optional bound is read through
1449
+ # `upper_bound_type` where the rbs version has it.
1450
+ def upper_bounded?(method_type, name)
1451
+ method_type.type_params.any? do |type_param|
1452
+ type_param.name == name &&
1453
+ (type_param.respond_to?(:upper_bound_type) ? type_param.upper_bound_type : type_param.upper_bound)
1454
+ end
1455
+ end
1456
+
614
1457
  # The `(variable name, bound type)` a positional parameter contributes, or {NO_BINDING} when it
615
1458
  # contributes none. The bare-variable shape is tried first because it is the overwhelmingly
616
1459
  # common one; the `Range[A]` shape is reached only when the parameter is not a bare variable.
@@ -759,11 +1602,11 @@ module Rigor
759
1602
 
760
1603
  # ----- block parameter probe (Phase C sub-phase 1) -----
761
1604
 
762
- def probe_block_param_types(receiver:, method_name:, args:, environment:)
1605
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
763
1606
  args ||= []
764
1607
  case receiver
765
- when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment)
766
- else probe_block_param_types_one(receiver, method_name, args, environment)
1608
+ when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
1609
+ else probe_block_param_types_one(receiver, method_name, args, environment, scope)
767
1610
  end
768
1611
  end
769
1612
 
@@ -771,9 +1614,9 @@ module Rigor
771
1614
  # member resolves the same arity and types (otherwise the call sites would have to thread
772
1615
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
773
1616
  # array so the binder defaults all params to Dynamic[Top].
774
- def probe_block_param_types_union(receiver, method_name, args, environment)
1617
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope)
775
1618
  results = receiver.members.map do |member|
776
- probe_block_param_types_one(member, method_name, args, environment)
1619
+ probe_block_param_types_one(member, method_name, args, environment, scope)
777
1620
  end
778
1621
  return [] if results.empty?
779
1622
  return [] unless results.all? { |r| r == results.first }
@@ -781,12 +1624,12 @@ module Rigor
781
1624
  results.first
782
1625
  end
783
1626
 
784
- def probe_block_param_types_one(receiver, method_name, args, environment)
1627
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope)
785
1628
  descriptor = receiver_descriptor(receiver)
786
1629
  return [] unless descriptor
787
1630
 
788
1631
  class_name, kind, receiver_args = descriptor
789
- method_definition = lookup_method(environment, class_name, kind, method_name)
1632
+ method_definition = lookup_method(environment, class_name, kind, method_name, scope)
790
1633
  return [] unless method_definition
791
1634
 
792
1635
  type_vars = build_type_vars(environment, class_name, receiver_args)
@@ -796,14 +1639,20 @@ module Rigor
796
1639
  kind: kind,
797
1640
  args: args,
798
1641
  type_vars: type_vars,
799
- environment: environment
1642
+ environment: environment,
1643
+ receiver: receiver,
1644
+ receiver_args: receiver_args,
1645
+ method_name: method_name
800
1646
  )
801
1647
  rescue StandardError
802
1648
  []
803
1649
  end
804
1650
 
1651
+ # rubocop:disable Metrics/ParameterLists
805
1652
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
806
- environment: nil)
1653
+ environment: nil, receiver: nil, receiver_args: [],
1654
+ method_name: nil)
1655
+ # rubocop:enable Metrics/ParameterLists
807
1656
  instance_type = Type::Combinator.nominal_of(class_name)
808
1657
  self_type =
809
1658
  case kind
@@ -811,10 +1660,24 @@ module Rigor
811
1660
  else instance_type
812
1661
  end
813
1662
 
1663
+ # Issue #1130 — a block parameter that receives `self` (`Object#tap` yields it) must see the
1664
+ # receiver's type arguments through the SAME {SelfSubstitute} verdict the return path applies
1665
+ # (#1092), so `ints.tap { |a| }` binds `a` to `Array[Integer]` rather than the raw `Array`.
1666
+ # The block path reuses only the keep-vs-degrade verdict, NOT the return path's value-pin
1667
+ # widening: the block parameter is a destructure source, so the receiver's OWN type arguments
1668
+ # (pinned constants included — `[1, 2].tap { |a, b| }` auto-splats the element union
1669
+ # `1 | 2` per slot, the parity an explicit `a, b = [1, 2]` gets) are the substitution the
1670
+ # binder should see. A verdict decline (an element-changing mutator like `map!`) keeps the
1671
+ # raw nominal, so that receiver's block parameter still arrives without its type arguments.
1672
+ substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
1673
+ self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
1674
+
814
1675
  method_type = OverloadSelector.select(
815
1676
  method_definition,
816
1677
  arg_types: args,
817
- self_type: self_type,
1678
+ # Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
1679
+ # the substitution verdict here is built from the receiver's type arguments alone.
1680
+ self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
818
1681
  instance_type: instance_type,
819
1682
  type_vars: type_vars,
820
1683
  block_required: true,