rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -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,471 @@ 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
+ answer = compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
570
+ memo[key] = answer if memo
571
+ answer
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
+ answer = compute_included_module_method(environment, class_name, method_name, scope)
602
+ memo[key] = answer if memo
603
+ answer
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 why a recording run bypasses 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
+ return nil if Rigor::Analysis::DependencyRecorder.active?
635
+
636
+ discovery = scope.discovery
637
+ slot = Thread.current[MIXIN_ANCESTOR_MEMO_KEY]
638
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
639
+ slot = [discovery, environment, {}]
640
+ Thread.current[MIXIN_ANCESTOR_MEMO_KEY] = slot
641
+ end
642
+ slot[2]
643
+ end
644
+
645
+ # The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
646
+ # the project's tables run LAST, only once a core / stdlib declaration is actually in hand.
647
+ # Every unresolved call on a Ruby-source receiver reaches here (`Widget.new.price` on a plain
648
+ # project class), and those walks read `Scope#superclass_of` / `#includes_of`, which file an
649
+ # ADR-46 ancestry edge. Running them unconditionally turned every cross-class METHOD call into
650
+ # a file-granular ancestry dependency — coarser than the symbol edge ADR-46 slice 4 files, and
651
+ # pinned against by `dependency_recorder_spec`. Reached at all, the edge is genuine: this
652
+ # answer does depend on the project not declaring the name on that ancestry.
653
+ def compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
654
+ return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
655
+ return nil if environment.plugin_registry&.open_receiver?(class_name)
656
+
657
+ definition, owner = Inference::ExternalAncestorResolution.resolve(
658
+ class_name, method_name, :instance,
659
+ scope: scope, environment: environment, record_dependencies: false, mixins: false
660
+ )
661
+ return nil if definition.nil?
662
+ return nil unless core_or_stdlib_owned?(environment, owner, definition)
663
+ return nil if returns_the_walked_ancestry?(definition, owner, environment)
664
+ return nil if dynamic_surface_through_ancestors?(scope, class_name)
665
+ return nil if project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
666
+ return nil if source_declares_through_ancestors?(scope, class_name, method_name)
667
+
668
+ definition
669
+ end
670
+
671
+ # The blocker this slice was first written without. CRuby PRESERVES THE SUBCLASS where core /
672
+ # stdlib RBS names the base class: `SubHash#merge` returns a `SubHash`, `SubSet#flatten` a
673
+ # `SubSet`, `SubPathname#basename` a `SubPathname`, `SubDate#+` a `SubDate` — verified against
674
+ # the interpreter. Adopting the declaration answers `Nominal[Hash]`, and because `Hash` IS
675
+ # RBS-known the negative rules then read it as a CLOSED surface: `sub.merge({}).own_method`
676
+ # drew an `error`-severity `call.undefined-method` on working code. That is ADR-5's failure
677
+ # exactly, one hop downstream — the propagation this slice's own boundary section names.
678
+ #
679
+ # RBS cannot distinguish the two families. `String#upcase: () -> String` really does return a
680
+ # plain `String` for a `String` subclass (Ruby 3.0 changed that), while `Hash#merge: () ->
681
+ # Hash[K, V]` really does return the subclass, and the two declarations are the same shape.
682
+ # So the answer is DECLINE, which is master's `Dynamic[top]` and therefore provably cannot
683
+ # regress a corpus target — rather than substituting the receiver as if the declaration read
684
+ # `-> self`. That substitution would be right for `merge` and wrong for `upcase`, and its
685
+ # wrongness is not purely a false negative: a `Nominal[SubStr]` that is really a `String`
686
+ # narrows `is_a?` guards and can reach `clause.unreachable` on a branch the runtime takes.
687
+ #
688
+ # `-> self` and `-> instance` returns are NOT affected and keep their precision: those already
689
+ # resolve against the receiver (`SubHash#clear` → `SubHash`, `SubStr#force_encoding` →
690
+ # `SubStr`, `MyError#exception` → `MyError`), which is what CRuby does.
691
+ #
692
+ # The test looks for the owner ANYWHERE in the return type, type arguments included. A first
693
+ # draft unwrapped only the top level, unions and optionals, on the reasoning that a class named
694
+ # inside `Array[...]` describes the elements rather than the returned object — true, and beside
695
+ # the point, because the ELEMENTS are subclass instances too. `Pathname#children: () ->
696
+ # Array[Pathname]` hands back an array of `SubPath`s (likewise `entries`, `each_child`,
697
+ # `ascend`, `descend`, `find`; only `glob` yields a plain `Pathname`), so
698
+ # `sub.children.first.own_method` fired the same `call.undefined-method` one level down.
699
+ # `Date#step`, `Date#upto` and `Set#classify` are the same family and escaped only by the shape
700
+ # of their declarations.
701
+ #
702
+ # The cost of the deeper walk is in the direction WD7 already accepts: `SubStr#chars` →
703
+ # `Array[String]` now declines although CRuby really does yield plain `String`s. `keys`,
704
+ # `to_a` and `classify` are unaffected, their arguments being type variables or `self`.
705
+ #
706
+ # An `Alias` that expands to the owner is not followed; that is a known gap in the FN direction.
707
+ def returns_the_walked_ancestry?(definition, owner, environment)
708
+ names = [owner.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)].to_set
709
+ method_types = definition.respond_to?(:method_types) ? definition.method_types : nil
710
+ return false if method_types.nil?
711
+
712
+ method_types.any? do |method_type|
713
+ return_type = method_type.type.respond_to?(:return_type) ? method_type.type.return_type : nil
714
+ mentions_class?(return_type, names)
715
+ end
716
+ rescue StandardError
717
+ # A signature whose return type cannot be read is a gap, and a gap declines.
718
+ true
719
+ end
720
+
721
+ # Whether any of `names` appears as a class instance anywhere in `type`, descending through
722
+ # every child an RBS type exposes — union members, the inside of an optional, and type
723
+ # ARGUMENTS. See {returns_the_walked_ancestry?}.
724
+ def mentions_class?(type, names, depth = 0)
725
+ return false if type.nil? || depth > RETURN_TYPE_UNWRAP_DEPTH
726
+ return true if own_class_name_matches?(type, names)
727
+ return false unless type.respond_to?(:each_type)
728
+
729
+ type.each_type.any? { |child| mentions_class?(child, names, depth + 1) }
730
+ end
731
+
732
+ def own_class_name_matches?(type, names)
733
+ type.is_a?(::RBS::Types::ClassInstance) && names.include?(type.name.to_s.delete_prefix("::"))
734
+ end
735
+
736
+ # A guard against a pathological or cyclic signature, not a semantic limit: real return types
737
+ # nest a level or two (`Array[Pathname]`, `Hash[Symbol, Array[String]]`).
738
+ RETURN_TYPE_UNWRAP_DEPTH = 8
739
+ private_constant :RETURN_TYPE_UNWRAP_DEPTH
740
+
741
+ # Issue #992's surface mark: a `Klass.include(M)` / `.prepend(M)` / `class_eval` written
742
+ # OUTSIDE the class body, which `ScopeIndexer` records as `ENVELOPE_DYNAMIC_MARK` because it
743
+ # can add members the in-body walks never see. `class Extended < Hash; end` followed by
744
+ # `Extended.include(Ext)` where `Ext#empty?` returns `42` must not adopt `Hash#empty?`. Asked
745
+ # of the receiver and of every SOURCE ancestor between it and the owner, because a mark on an
746
+ # intermediate reaches the receiver just as well.
747
+ def dynamic_surface_through_ancestors?(scope, class_name)
748
+ return true if dynamic_surface?(scope, class_name)
749
+
750
+ each_source_ancestor_candidate(scope, class_name) do |candidate|
751
+ return true if dynamic_surface?(scope, candidate)
752
+ end
753
+ false
754
+ end
755
+
756
+ def dynamic_surface?(scope, class_name)
757
+ Scope::DiscoveryIndex.rewritten_surface?(scope.parameter_envelopes_of(class_name))
758
+ end
759
+
760
+ # ADR-110's precedence, asked of both tables the project's own members land in. Either one
761
+ # answering true is a decline; both suppress (answer true) when their shared
762
+ # `Scope::ANCESTOR_WALK_LIMIT` budget runs out, and record a `BudgetTrace` hit there.
763
+ def source_declares_through_ancestors?(scope, class_name, method_name)
764
+ return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
765
+
766
+ !scope.user_def_through_ancestors(class_name, method_name).first.nil?
767
+ end
768
+
769
+ # Both ends of the declaration have to be core / stdlib: the ancestor the walk asked (`Hash`,
770
+ # `StringScanner`) and the class the declaration is written on (`Exception` for
771
+ # `StandardError#message`, `Comparable` for a `clamp`). Either being a gem's or the project's
772
+ # own RBS is slice 3's question, not this one's.
773
+ def core_or_stdlib_owned?(environment, owner, definition)
774
+ loader = environment.rbs_loader
775
+ return false if loader.nil? || !loader.respond_to?(:core_or_stdlib_class?)
776
+ return false unless loader.core_or_stdlib_class?(owner)
777
+
778
+ declared_on = definition.respond_to?(:defined_in) ? definition.defined_in : nil
779
+ return false if declared_on.nil?
780
+
781
+ loader.core_or_stdlib_class?(declared_on.to_s)
782
+ end
783
+
784
+ # ADR-17 — a `pre_eval:` file that reopens the receiver, any SOURCE ancestor between it and the
785
+ # owner, or any RBS ancestor of the owner, and redefines the name. The declaration this arm
786
+ # would adopt is then not the method that runs. The source chain is the half the first draft
787
+ # missed: `class Middle < Hash; end; class Leaf < Middle; end` with a `pre_eval:`
788
+ # `class Middle; def key?(k) = 42; end` answered `bool` for a call that returns `42`.
789
+ def project_patched_through_ancestors?(environment, scope, class_name, owner, method_name)
790
+ patched = environment.project_patched_methods
791
+ return false if patched.nil? || patched.empty?
792
+
793
+ owners = [class_name.to_s.delete_prefix("::"), *rbs_instance_ancestor_names(owner, environment)]
794
+ each_source_ancestor_candidate(scope, class_name) { |candidate| owners << candidate }
795
+ owners.any? do |name|
796
+ !patched.lookup(class_name: name, method_name: method_name, kind: :instance).nil?
797
+ end
798
+ end
799
+
800
+ # Memo for the whole decision. Every call site of a class asks the same `(class, method)`
801
+ # question, and the answer is a pure function of the frozen discovery index and the
802
+ # environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
803
+ # dependency edges bypasses it: the shadow probes above read the project's method tables, and
804
+ # a memo would swallow that edge for every file after the first.
805
+ #
806
+ # ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
807
+ # the measurement. A `Scope` hands each analysed file its own discovery index, so an
808
+ # identity-keyed store would pin every file's index, and every RBS definition resolved
809
+ # against it, for the length of the run.
810
+ CORE_STDLIB_ANCESTOR_MEMO_KEY = :__rigor_core_stdlib_ancestor_dispatch__
811
+ private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
812
+
813
+ def core_stdlib_memo(environment, scope)
814
+ return nil if Rigor::Analysis::DependencyRecorder.active?
815
+
816
+ discovery = scope.discovery
817
+ slot = Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY]
818
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
819
+ slot = [discovery, environment, {}]
820
+ Thread.current[CORE_STDLIB_ANCESTOR_MEMO_KEY] = slot
821
+ end
822
+ slot[2]
823
+ end
824
+
825
+ # BFS over the scope's as-written ancestry tables — include edges first, then the
826
+ # superclass, matching the MRO — yielding every ancestor-name candidate. The tables store
827
+ # names AS WRITTEN — `"::API::Base"`, bare `"Base"` — so each hop resolves through the
828
+ # nesting-aware `ancestor_name_candidates` rather than a raw lookup. Deliberately
829
+ # NOT `external_ancestor_name_candidates`: that walk records `ancestry_sources` edges via
830
+ # `record_class_dependency`, which would mislabel a dispatch lookup as an ancestry edge.
831
+ #
832
+ # Issue #1173 — the walk follows include edges too, because a shadow guard that reads only
833
+ # the superclass chain misses the nearer half of the ancestry: `class C < Base; include M;
834
+ # end` chains `C → M → Base`, and a `def`, an outside-the-body mark, or a `pre_eval:` patch
835
+ # on a source `M` all outrank `Base`'s declaration. A candidate that names a project class /
836
+ # module continues the walk ({Scope#known_user_class?} — a module that defines nothing and
837
+ # mixes nothing in still gates what the arm may adopt); an RBS-known or unresolved one is
838
+ # yielded for the caller's guards but its ancestry is the RBS side's business. The include
839
+ # table is read RAW (`scope.discovered_includes`), the same reason the superclass table is:
840
+ # `Scope#includes_of` files `record_class_dependency` on every read — a miss included —
841
+ # and this walk runs for every unresolved call on a project class (`Widget.new`), so the
842
+ # reader would turn each one into an ancestry edge ADR-46 slice 4 keeps at symbol
843
+ # granularity (`dependency_recorder_spec`).
844
+ def each_source_ancestor_candidate(scope, class_name)
845
+ supers = scope.discovered_superclasses
846
+ includes = scope.discovered_includes
847
+ queue = [class_name.to_s]
848
+ seen = {}
849
+ until queue.empty?
850
+ current = queue.shift
851
+ next if current.nil? || seen[current]
852
+
853
+ seen[current] = true
854
+ ((includes[current] || []) + [supers[current]].compact).each do |raw|
855
+ scope.ancestor_name_candidates(current, raw).each do |candidate|
856
+ yield candidate
857
+ queue << candidate if scope.known_user_class?(candidate)
858
+ end
859
+ end
860
+ end
861
+ end
862
+
863
+ # The extend-edge twin of `allowed_rbs_complete_ancestor` (manifest `rbs_complete_extends:`).
864
+ # `extend M` lifts M's INSTANCE surface onto the extending class object's singleton, so a
865
+ # singleton call on a Ruby-source class can resolve through a module the class — or one of
866
+ # its discovered superclasses — extends. Returns the first resolved candidate name that a
867
+ # loaded plugin allow-lists, or nil. Same guards as the superclass bridge, minus the
868
+ # RBS-known receiver exit: the direct lookup has already missed by the time this runs, and
869
+ # a class that is BOTH source-defined and RBS-known (`class F` in `sig/` plus `extend T::Sig`
870
+ # in the body) still carries the source edge — the runtime ancestry contains the module
871
+ # either way, so withholding the bridge would leave a real `sig` opaque. A nearer source
872
+ # `def self.x` still shadows any bridged module method.
873
+ def allowed_rbs_complete_extended_module(environment, class_name, method_name, scope,
874
+ call_node = nil)
875
+ return nil if scope.nil?
876
+
877
+ registry = environment&.plugin_registry
878
+ return nil if registry.nil?
879
+
381
880
  supers = scope.discovered_superclasses
881
+ extends = scope.discovered_extends
882
+ queue = [class_name.to_s]
382
883
  seen = {}
383
- current = supers[class_name.to_s]
384
- until current.nil? || seen[current]
385
- return current if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(current)
884
+ until queue.empty?
885
+ current = queue.shift
886
+ next if current.nil? || seen[current]
386
887
 
387
888
  seen[current] = true
388
- current = supers[current]
889
+ # The class's own `def self.x` sits ahead of every `extend` — once it has run.
890
+ # `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
891
+ # written AFTER this `sig {}` does not suppress the bridge.
892
+ return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
893
+
894
+ resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
895
+ registry, method_name, call_node)
896
+ return nil if resolved.equal?(EXTEND_OWNER_STOP)
897
+ return resolved if resolved
898
+
899
+ raw = supers[current]
900
+ scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
901
+ end
902
+ nil
903
+ end
904
+
905
+ # One walk hop of `allowed_rbs_complete_extended_module`. Each `extend` edge binds to the
906
+ # first resolution candidate that exists at runtime. If that owner DEFINES `method_name`:
907
+ # an allow-listed RBS module is the answer; a project class or a non-allow-listed RBS name
908
+ # owns the call, so the hop returns {EXTEND_OWNER_STOP} and the outer walk must not search
909
+ # later extends or superclasses (a nested `Outer::CustomSig` would otherwise fall through
910
+ # to `T::Sig` and type the call as `nil`). An owner that does not define the method yields
911
+ # to the next extended module — Ruby's singleton ancestry searches every extend in turn.
912
+ def rbs_complete_extended_module_for(current, extends, environment, scope, registry,
913
+ method_name, call_node)
914
+ each_extended_module_name(current, extends, environment) do |mod_name|
915
+ owner = scope.ancestor_name_candidates(current, mod_name).find do |candidate|
916
+ scope.known_user_class?(candidate) ||
917
+ Rigor::Reflection.rbs_class_known?(candidate, environment: environment)
918
+ end
919
+ next if owner.nil?
920
+ next unless extend_owner_defines?(owner, method_name, call_node, scope, environment)
921
+
922
+ project_owned = scope.known_user_class?(owner)
923
+ return owner if !project_owned && registry.rbs_complete_extends?(owner)
924
+
925
+ return EXTEND_OWNER_STOP
389
926
  end
390
927
  nil
391
928
  end
392
929
 
930
+ def extend_owner_defines?(owner, method_name, call_node, scope, environment)
931
+ return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
932
+
933
+ !lookup_method_on(environment, owner, :instance, method_name).nil?
934
+ end
935
+
936
+ # Sentinel: a nearer `extend` answers `method_name`, so the allow-list must not continue.
937
+ EXTEND_OWNER_STOP = :__rbs_complete_extends_stop__
938
+ private_constant :EXTEND_OWNER_STOP
939
+
940
+ # The module names `current` extends, source table first (`discovered_extends`, stored in
941
+ # singleton-ancestor search order — nearest edge first) then the RBS side
942
+ # (`singleton_extended_modules`, already qualified) — an RBS superclass like `T::Struct`
943
+ # declares `extend T::Props::ClassMethods` in signature, and a source subclass inherits it.
944
+ def each_extended_module_name(current, extends, environment, &)
945
+ (extends[current] || []).each(&)
946
+ (environment&.singleton_extended_modules(current) || []).each(&)
947
+ end
948
+
393
949
  # 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.
950
+ # receiver's `type_args`. Returns an empty hash when either side is empty or when the receiver
951
+ # carries MORE arguments than the class declares -- in every such case free variables in the
952
+ # method's return type degrade to `Dynamic[Top]` per the translator's contract.
953
+ #
954
+ # Issue #1121 -- FEWER arguments than parameters is not a disagreement, it is the partial
955
+ # application RBS itself licenses for a class whose trailing parameters declare a default:
956
+ # `Enumerator::Lazy[out E, out R = void]` is spelled `Enumerator::Lazy[Elem]` by the very
957
+ # signature that hands one back (`Enumerable#lazy: () -> Enumerator::Lazy[Elem]`). Withholding
958
+ # the whole map there dropped the element binding on every lazy-enumerator receiver, so the
959
+ # block parameter of `lazy.map { |x| … }` was `Dynamic[top]` and the chain could not recover the
960
+ # element type. The supplied prefix MUST bind in declaration order and the omitted trailing names
961
+ # stay unbound (`Dynamic[top]`), exactly as any other free variable does.
397
962
  def build_type_vars(environment, class_name, receiver_args)
398
963
  return NO_TYPE_VARS if receiver_args.empty?
399
964
 
400
965
  param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
401
966
  return NO_TYPE_VARS if param_names.empty?
402
- return NO_TYPE_VARS if param_names.size != receiver_args.size
967
+ return NO_TYPE_VARS if receiver_args.size > param_names.size
403
968
 
404
- param_names.zip(receiver_args).to_h
969
+ param_names.first(receiver_args.size).zip(receiver_args).to_h
405
970
  end
406
971
 
407
972
  # The shared empty substitution map: most receivers carry no type arguments, and the translator
@@ -428,13 +993,16 @@ module Rigor
428
993
  end
429
994
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
430
995
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
431
- # fallback returns the caller's type, not Object.
996
+ # fallback returns the caller's type, not Object. `dispatch_one` also routes the receiver's
997
+ # type-argument-bearing projection through it ({SelfSubstitute}, #1092).
432
998
  self_type = self_type_override || resolved_self_type
433
999
 
434
1000
  candidates = OverloadSelector.select_candidates(
435
1001
  method_definition,
436
1002
  arg_types: args,
437
- self_type: self_type,
1003
+ # A `Dynamic` self (#1092) is a return-side answer; overload selection and ReceiverAffinity
1004
+ # read the static facet, as they did before the substitute carried the wrapping.
1005
+ self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
438
1006
  instance_type: instance_type,
439
1007
  type_vars: type_vars,
440
1008
  block_required: !block_type.nil?,
@@ -446,6 +1014,7 @@ module Rigor
446
1014
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
447
1015
  join_candidate_returns(
448
1016
  candidates,
1017
+ method_definition: method_definition,
449
1018
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
450
1019
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
451
1020
  alias_expander: environment.rbs_loader
@@ -485,17 +1054,20 @@ module Rigor
485
1054
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
486
1055
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
487
1056
  # 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)
1057
+ def join_candidate_returns(candidates, method_definition:, self_type:, instance_type:, type_vars:, args:,
1058
+ block_type:, scope:, call_node:, call_site:, alias_expander: nil)
490
1059
  returns = candidates.map do |method_type|
491
1060
  full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
492
- RbsTypeTranslator.translate(
1061
+ returned = RbsTypeTranslator.translate(
493
1062
  method_type.type.return_type,
494
1063
  self_type: self_type,
495
1064
  instance_type: instance_type,
496
1065
  type_vars: full_type_vars,
497
1066
  alias_expander: alias_expander
498
1067
  )
1068
+ next returned unless combining_overload?(method_definition, method_type, call_site)
1069
+
1070
+ class_level_sum(returned, method_type, args)
499
1071
  end
500
1072
  return returns.first if returns.size == 1
501
1073
  return nil if returns.any?(&:nil?)
@@ -507,11 +1079,100 @@ module Rigor
507
1079
  end
508
1080
 
509
1081
  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)
1082
+ vars = compose_block_type_vars(method_type, type_vars, block_type, args,
1083
+ scope: scope, call_node: call_node, call_site: call_site)
511
1084
  compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
512
1085
  call_site: call_site)
513
1086
  end
514
1087
 
1088
+ # Whether the overload is one `Enumerable` declares for `sum`, whose declared return the tier reads
1089
+ # at class level ({#class_level_sum}). Every overload spells its return as the sides the method adds
1090
+ # together, as a union or as one variable that covers both: `() -> (E | Integer)`,
1091
+ # `[T] () { (E) -> T } -> (Integer | T)`, `[T] (?T) -> (E | T)` and `[U] (?U) { (E) -> U } -> U`.
1092
+ # A value is not closed under that addition, so the value-pinned bindings of `E` (from the
1093
+ # receiver), `T` (from the argument, issue #303) and the block's type do not describe the result:
1094
+ # `[1, 2].each.sum(0.0)` is `3.0`, which `0.0 | 1 | 2` misses. `Array#fetch: [T] (int, T default)
1095
+ # -> (E | T)` is spelled the same way but
1096
+ # returns the default object itself, so the signature alone cannot tell the two apart and the
1097
+ # declaring module does. A class that declares its own `sum` states its own contract and keeps it.
1098
+ def combining_overload?(method_definition, method_type, call_site)
1099
+ return false unless call_site[1] == :sum
1100
+
1101
+ type_def = OptimisticOrigin.matching_type_def(method_definition, method_type)
1102
+ !type_def.nil? && type_def.defined_in.to_s.delete_prefix("::") == "Enumerable"
1103
+ end
1104
+
1105
+ # Apart from the range shortcut ({#range_coerced_seed?}), CRuby's `enum_sum` adds each value to an
1106
+ # accumulator that starts at the seed. Between two of these classes `+` answers the later one
1107
+ # (`1 + 0.5` and `0.5 + 1` are Floats, `1 + 1r` is a Rational), so the accumulator's class only ever
1108
+ # moves up this order.
1109
+ SUM_PROMOTION_RANKS = { "Integer" => 0, "Rational" => 1, "Float" => 2, "Complex" => 3 }.freeze
1110
+ private_constant :SUM_PROMOTION_RANKS
1111
+
1112
+ # CRuby's seed when the call passes none.
1113
+ SUM_DEFAULT_SEED = ["Integer"].freeze
1114
+ private_constant :SUM_DEFAULT_SEED
1115
+
1116
+ # The seeds CRuby's integer-range shortcut adds to with plain `+` ({#range_coerced_seed?}).
1117
+ RANGE_ADDED_SEEDS = Set["Integer", "Float"].freeze
1118
+ private_constant :RANGE_ADDED_SEEDS
1119
+
1120
+ # The class-level reading of a `sum` overload's translated return, or `Dynamic[top]` when a member
1121
+ # widens to none of the value classes {#value_classes} admits. A class, not a value, is what the
1122
+ # addition keeps closed: `sum` over `0.0` and `1 | 2` is `3.0`, a Float, and over `1.5 | 2.5` is
1123
+ # `4.0`.
1124
+ #
1125
+ # The union of the members' classes would still read wider than the runtime, because the seed
1126
+ # promotes every value it absorbs: `ints.each.sum(0.0)` is always a Float, and `Float | Integer`
1127
+ # fires `def.return-type-mismatch` against a declared `-> Float` on correct code. So each class is
1128
+ # taken as the class the accumulator reaches from each seed class ({#promoted_class}), and the seed
1129
+ # itself stays for a receiver that yields nothing: `ints.each.sum(0.0)` reads `Float`, and
1130
+ # `floats.each.sum(0)` reads `Float | Integer`, whose Integer is the empty receiver's `0`.
1131
+ def class_level_sum(type, method_type, args)
1132
+ return nil if type.nil?
1133
+
1134
+ members = value_classes(type)
1135
+ return Type::Combinator.untyped if members.nil?
1136
+
1137
+ seeds = sum_seed_classes(method_type, args)
1138
+ return Type::Combinator.untyped if seeds.nil? || range_coerced_seed?(method_type, seeds)
1139
+
1140
+ reached = seeds.flat_map { |seed| members.map { |member| promoted_class(seed, member) } }
1141
+ Type::Combinator.union(*(seeds | reached).map { |name| Type::Combinator.nominal_of(name) })
1142
+ end
1143
+
1144
+ # Whether CRuby may skip the accumulator and read the seed as a Float. With no block and a seed that
1145
+ # is not a Float, `enum_sum` sums a range with Integer endpoints by Gauss's formula and adds the
1146
+ # result to the seed. An Integer seed takes plain `+`; any other goes through `Integer#coerce`, which
1147
+ # converts it with `Float()`: `(1..3).sum(0r)` is `6.0` and `(1..3).sum("1.5")` is `7.5`. Any object
1148
+ # that answers `begin`, `end` and `exclude_end?` takes the same path, so the receiver's class cannot
1149
+ # rule it out.
1150
+ def range_coerced_seed?(method_type, seeds)
1151
+ method_type.block.nil? && seeds.any? { |seed| !RANGE_ADDED_SEEDS.include?(seed) }
1152
+ end
1153
+
1154
+ # The classes of the value `sum` starts from: the argument when the overload takes one and the call
1155
+ # passes it, and CRuby's `0` otherwise.
1156
+ def sum_seed_classes(method_type, args)
1157
+ fun = method_type.type
1158
+ return SUM_DEFAULT_SEED if args.empty? || !fun.respond_to?(:required_positionals)
1159
+ return SUM_DEFAULT_SEED if fun.required_positionals.empty? && fun.optional_positionals.empty?
1160
+
1161
+ value_classes(args.first)
1162
+ end
1163
+
1164
+ # The class the accumulator reaches when a `member` value is added to a `seed`-class one. `+` does not
1165
+ # add a String and a number (`"" + 1` and `1 + ""` raise), so such a pair keeps the member's class,
1166
+ # which reads wider than the runtime rather than narrower. The range shortcut that does convert a
1167
+ # String seed never reaches here ({#range_coerced_seed?}).
1168
+ def promoted_class(seed, member)
1169
+ seed_rank = SUM_PROMOTION_RANKS[seed]
1170
+ member_rank = SUM_PROMOTION_RANKS[member]
1171
+ return member if seed_rank.nil? || member_rank.nil?
1172
+
1173
+ seed_rank > member_rank ? seed : member
1174
+ end
1175
+
515
1176
  # Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
516
1177
  # `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
517
1178
  # kind]` triple the {VoidOrigin} carries.
@@ -558,13 +1219,160 @@ module Rigor
558
1219
  # block return type at the same call site. Anything outside this exact shape (no block clause,
559
1220
  # an `untyped` block, a non-variable block return type, a variable not declared in
560
1221
  # `type_params`) returns the original `type_vars` so fallbacks stay consistent.
561
- def compose_block_type_vars(method_type, type_vars, block_type)
1222
+ #
1223
+ # The block alone does not decide a variable that a parameter's type also names once the call
1224
+ # passes an argument. `Enumerable#sum: [U] (?U) { (E) -> U } -> U` adds the block's values to the
1225
+ # argument, `Enumerable#inject: [A] (A initial) { (A, E) -> A } -> A` returns the argument for an
1226
+ # empty receiver, and `Hash#transform_keys: [K2] (hash[_Key, K2]) { (K) -> K2 } -> Hash[K2, V]`
1227
+ # takes a mapping hit's value without yielding. `Dynamic[block_type]` would not do: a `Dynamic`
1228
+ # receiver dispatches through its static facet and answers exactly, so a facet that misses the
1229
+ # runtime value is wrong one call later (`(h.sum(0.0) { |_k, v| v } / h.size).nan?` read
1230
+ # `Integer#nan?`). Nor would joining in the argument as it stands, since
1231
+ # `[1, 2].each.sum(0.0) { |x| x }` is `3.0`, which neither side contains. The variable is bound
1232
+ # to {#shared_value_class} where both sides have one, and to `Dynamic[top]` otherwise. The key
1233
+ # stays in the map either way so {#compose_arg_type_vars} does not bind the variable from the
1234
+ # argument alone.
1235
+ def compose_block_type_vars(method_type, type_vars, block_type, args, scope:, call_node:, call_site:)
562
1236
  return type_vars if block_type.nil?
563
1237
 
564
1238
  block_var_name = method_type_block_return_variable(method_type)
565
1239
  return type_vars if block_var_name.nil?
1240
+ return type_vars.merge(block_var_name => block_type) unless
1241
+ argument_reaches_variable?(method_type, block_var_name, args)
1242
+
1243
+ shared = shared_value_class(method_type, block_var_name, args, block_type, call_node)
1244
+ bound = shared && arg_binding_permitted?(scope, call_node, call_site) ? shared : Type::Combinator.untyped
1245
+ type_vars.merge(block_var_name => bound)
1246
+ end
566
1247
 
567
- type_vars.merge(block_var_name => block_type)
1248
+ # Whether an argument the call passes may land in a parameter whose type names `name`, anywhere
1249
+ # inside it (`hash[_Key, K2]` names `K2`). The argument count is not matched against the
1250
+ # parameter list: a `*splat` argument stands for any number of arguments, and keyword arguments
1251
+ # arrive as one more entry in `args`, so any argument counts as reaching every parameter.
1252
+ def argument_reaches_variable?(method_type, name, args)
1253
+ return false if args.empty?
1254
+
1255
+ method_type.type.each_param.any? { |param| mentions_variable?(param.type, name) }
1256
+ end
1257
+
1258
+ # The one value class that the arguments landing in `name`'s parameters and the block's type all
1259
+ # share, or nil to leave the variable `Dynamic[top]`. A class, not a value, because a combining
1260
+ # method keeps a class closed and not a value: `[1, 2].each.sum(1) { |x| -x }` is `-2`, which
1261
+ # neither `1` nor `-1 | -2` contains. RBS reads `[U] (?U) { (E) -> U } -> U` the same way, as a
1262
+ # `U` that covers the argument and the block alike. One class and not a union of several, because
1263
+ # `sum` absorbs: over Integers, `sum(0.0)` answers a Float every time, and `Float | Integer` would
1264
+ # fire `def.return-type-mismatch` against a declared `-> Float`.
1265
+ #
1266
+ # The block's type is one typing of its body, so it covers every call only when nothing the block
1267
+ # receives depends on the variable ({#block_receives_variable?}). The argument binding's gate
1268
+ # applies ({#arg_binding_permitted?}), since this reads the argument. The positions must be static
1269
+ # ({#arguments_at_variable}), and every side must widen to a class ({#value_classes}). The block's
1270
+ # type is trusted as far as the engine trusts it, so a hash filled through an alias, which reads
1271
+ # narrower than it is, binds its narrower class here too.
1272
+ def shared_value_class(method_type, name, args, block_type, call_node)
1273
+ return nil if block_receives_variable?(method_type.block, name)
1274
+
1275
+ reaching = arguments_at_variable(method_type, name, args, call_node)
1276
+ return nil if reaching.nil?
1277
+
1278
+ sides = [*reaching, block_type].map { |type| value_classes(type) }
1279
+ return nil unless sides.all?
1280
+
1281
+ classes = sides.flatten(1).uniq
1282
+ classes.size == 1 ? Type::Combinator.nominal_of(classes.first) : nil
1283
+ end
1284
+
1285
+ # Whether the block's parameters or its `self` name the variable, as `inject`'s accumulator
1286
+ # (`{ (A, E) -> A }`) and `produce`'s previous element (`{ (T prev) -> T }`) do. Such a parameter
1287
+ # holds the argument on the first call and the block's own result after it, and the pass that
1288
+ # typed the block typed it once, as whatever reached it: the RBS probe leaves it untyped, but
1289
+ # `IteratorDispatch` hands an Array receiver's `inject` the seed, and a `&:+` block then reads
1290
+ # `0.+`, so `[1.5, 2].inject(0, &:+)`, which is `3.5`, would bind `Integer`.
1291
+ def block_receives_variable?(block, name)
1292
+ return true if block.self_type && mentions_variable?(block.self_type, name)
1293
+
1294
+ block.type.each_param.any? { |param| mentions_variable?(param.type, name) }
1295
+ end
1296
+
1297
+ # The arguments that land in a positional parameter whose whole type is `name`, or nil when the
1298
+ # call's positions or the signature's shape leave that open. The call must pass plain positional
1299
+ # arguments: after a `*splat` that turns out empty, the next argument lands one parameter earlier,
1300
+ # and a forwarded `...` or keyword arguments may land anywhere. The signature must name `name` only
1301
+ # as a whole leading positional parameter's type, and have no trailing positional parameter at
1302
+ # all. One that names the variable takes its argument from the end of the list, which pairing the
1303
+ # leading parameters with the arguments in order would miss; one that does not is declined too,
1304
+ # conservatively.
1305
+ def arguments_at_variable(method_type, name, args, call_node)
1306
+ return nil unless plain_positional_arguments?(call_node, args.size)
1307
+
1308
+ fun = method_type.type
1309
+ return nil unless fun.respond_to?(:trailing_positionals) && fun.trailing_positionals.empty?
1310
+ return nil if named_outside_positionals?(fun, name)
1311
+
1312
+ positionals = fun.required_positionals + fun.optional_positionals
1313
+ positionals.zip(args).each_with_object([]) do |(param, arg), reaching|
1314
+ type = param.type
1315
+ next unless mentions_variable?(type, name)
1316
+ return nil unless type.is_a?(RBS::Types::Variable)
1317
+
1318
+ reaching << arg unless arg.nil?
1319
+ end
1320
+ end
1321
+
1322
+ def plain_positional_arguments?(call_node, count)
1323
+ return false unless call_node.is_a?(Prism::CallNode)
1324
+
1325
+ arguments = call_node.arguments&.arguments || EMPTY_ARGUMENT_NODES
1326
+ arguments.size == count && arguments.none? do |argument|
1327
+ case argument
1328
+ when Prism::SplatNode, Prism::ForwardingArgumentsNode, Prism::KeywordHashNode then true
1329
+ else false
1330
+ end
1331
+ end
1332
+ end
1333
+
1334
+ def named_outside_positionals?(fun, name)
1335
+ others = [fun.rest_positionals, fun.rest_keywords].compact +
1336
+ fun.required_keywords.values + fun.optional_keywords.values
1337
+ others.any? { |param| mentions_variable?(param.type, name) }
1338
+ end
1339
+
1340
+ # The class names `type`'s union members widen to, or nil when a member widens to none of
1341
+ # {CLOSED_VALUE_CLASSES}.
1342
+ def value_classes(type)
1343
+ members = type.is_a?(Type::Union) ? type.members : [type]
1344
+ classes = members.map { |member| value_class(member) }
1345
+ classes.all? ? classes : nil
1346
+ end
1347
+
1348
+ # A literal widens to its value's class, a bounded number to `Integer` or `Float`, and a refinement
1349
+ # or a difference to its base's class. Everything else declines: a generic class (`[] + [:a]`
1350
+ # concatenates elements rather than keeping either side's), a carrier with no class (a tuple, a
1351
+ # hash shape, `Dynamic`), and `nil` or `NilClass`, whose arm would fire
1352
+ # `call.possible-nil-receiver` where `Array#max` and `#first` bet on a non-empty receiver.
1353
+ def value_class(type)
1354
+ case type
1355
+ when Type::Nominal then closed_value_class(type.class_name)
1356
+ when Type::Constant then closed_value_class(type.value.class.name)
1357
+ when Type::IntegerRange then "Integer"
1358
+ when Type::FloatRange then "Float"
1359
+ when Type::Refined, Type::Difference then value_class(type.base)
1360
+ end
1361
+ end
1362
+
1363
+ def closed_value_class(class_name)
1364
+ name = class_name&.delete_prefix("::")
1365
+ CLOSED_VALUE_CLASSES.include?(name) ? name : nil
1366
+ end
1367
+
1368
+ # A signature nested past {RETURN_TYPE_UNWRAP_DEPTH} counts as naming the variable, which is the
1369
+ # untyped answer.
1370
+ def mentions_variable?(type, name, depth = 0)
1371
+ return true if depth > RETURN_TYPE_UNWRAP_DEPTH
1372
+ return type.name == name if type.is_a?(::RBS::Types::Variable)
1373
+ return false unless type.respond_to?(:each_type)
1374
+
1375
+ type.each_type.any? { |child| mentions_variable?(child, name, depth + 1) }
568
1376
  end
569
1377
 
570
1378
  # Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
@@ -607,10 +1415,25 @@ module Rigor
607
1415
  name, bound = param_binding(param, arg, declared, type_vars)
608
1416
  next if name.nil?
609
1417
 
1418
+ bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
610
1419
  bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
611
1420
  end
612
1421
  end
613
1422
 
1423
+ # Issue #1347 — a variable that declares an upper bound (`[T < X]`) binds the argument widened off its
1424
+ # value-pinned members. The bound constrains a class, and `Rational#*: [T < Numeric](T) -> T` returns a
1425
+ # value of the argument's class, not the argument, so `r * 0.5` read the literal `0.5`. An unbounded
1426
+ # variable keeps the literal (`Ractor.make_shareable("x")` is `"x"`); a bounded identity return
1427
+ # (`String#setbyte`) gives its literal up. Any bound counts: `upper_bound` answers only a class, singleton
1428
+ # or interface bound, so an alias, union, intersection or optional bound is read through
1429
+ # `upper_bound_type` where the rbs version has it.
1430
+ def upper_bounded?(method_type, name)
1431
+ method_type.type_params.any? do |type_param|
1432
+ type_param.name == name &&
1433
+ (type_param.respond_to?(:upper_bound_type) ? type_param.upper_bound_type : type_param.upper_bound)
1434
+ end
1435
+ end
1436
+
614
1437
  # The `(variable name, bound type)` a positional parameter contributes, or {NO_BINDING} when it
615
1438
  # contributes none. The bare-variable shape is tried first because it is the overwhelmingly
616
1439
  # common one; the `Range[A]` shape is reached only when the parameter is not a bare variable.
@@ -759,11 +1582,11 @@ module Rigor
759
1582
 
760
1583
  # ----- block parameter probe (Phase C sub-phase 1) -----
761
1584
 
762
- def probe_block_param_types(receiver:, method_name:, args:, environment:)
1585
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
763
1586
  args ||= []
764
1587
  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)
1588
+ when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment, scope)
1589
+ else probe_block_param_types_one(receiver, method_name, args, environment, scope)
767
1590
  end
768
1591
  end
769
1592
 
@@ -771,9 +1594,9 @@ module Rigor
771
1594
  # member resolves the same arity and types (otherwise the call sites would have to thread
772
1595
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
773
1596
  # array so the binder defaults all params to Dynamic[Top].
774
- def probe_block_param_types_union(receiver, method_name, args, environment)
1597
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope)
775
1598
  results = receiver.members.map do |member|
776
- probe_block_param_types_one(member, method_name, args, environment)
1599
+ probe_block_param_types_one(member, method_name, args, environment, scope)
777
1600
  end
778
1601
  return [] if results.empty?
779
1602
  return [] unless results.all? { |r| r == results.first }
@@ -781,12 +1604,12 @@ module Rigor
781
1604
  results.first
782
1605
  end
783
1606
 
784
- def probe_block_param_types_one(receiver, method_name, args, environment)
1607
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope)
785
1608
  descriptor = receiver_descriptor(receiver)
786
1609
  return [] unless descriptor
787
1610
 
788
1611
  class_name, kind, receiver_args = descriptor
789
- method_definition = lookup_method(environment, class_name, kind, method_name)
1612
+ method_definition = lookup_method(environment, class_name, kind, method_name, scope)
790
1613
  return [] unless method_definition
791
1614
 
792
1615
  type_vars = build_type_vars(environment, class_name, receiver_args)
@@ -796,14 +1619,20 @@ module Rigor
796
1619
  kind: kind,
797
1620
  args: args,
798
1621
  type_vars: type_vars,
799
- environment: environment
1622
+ environment: environment,
1623
+ receiver: receiver,
1624
+ receiver_args: receiver_args,
1625
+ method_name: method_name
800
1626
  )
801
1627
  rescue StandardError
802
1628
  []
803
1629
  end
804
1630
 
1631
+ # rubocop:disable Metrics/ParameterLists
805
1632
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
806
- environment: nil)
1633
+ environment: nil, receiver: nil, receiver_args: [],
1634
+ method_name: nil)
1635
+ # rubocop:enable Metrics/ParameterLists
807
1636
  instance_type = Type::Combinator.nominal_of(class_name)
808
1637
  self_type =
809
1638
  case kind
@@ -811,10 +1640,24 @@ module Rigor
811
1640
  else instance_type
812
1641
  end
813
1642
 
1643
+ # Issue #1130 — a block parameter that receives `self` (`Object#tap` yields it) must see the
1644
+ # receiver's type arguments through the SAME {SelfSubstitute} verdict the return path applies
1645
+ # (#1092), so `ints.tap { |a| }` binds `a` to `Array[Integer]` rather than the raw `Array`.
1646
+ # The block path reuses only the keep-vs-degrade verdict, NOT the return path's value-pin
1647
+ # widening: the block parameter is a destructure source, so the receiver's OWN type arguments
1648
+ # (pinned constants included — `[1, 2].tap { |a, b| }` auto-splats the element union
1649
+ # `1 | 2` per slot, the parity an explicit `a, b = [1, 2]` gets) are the substitution the
1650
+ # binder should see. A verdict decline (an element-changing mutator like `map!`) keeps the
1651
+ # raw nominal, so that receiver's block parameter still arrives without its type arguments.
1652
+ substitute = SelfSubstitute.for(receiver, receiver_args, method_name, args, nil)
1653
+ self_type = Type::Combinator.nominal_of(class_name, type_args: receiver_args) if substitute
1654
+
814
1655
  method_type = OverloadSelector.select(
815
1656
  method_definition,
816
1657
  arg_types: args,
817
- self_type: self_type,
1658
+ # Overload selection reads a `Dynamic` self's static facet, mirroring the return path;
1659
+ # the substitution verdict here is built from the receiver's type arguments alone.
1660
+ self_type: self_type.is_a?(Type::Dynamic) ? self_type.static_facet : self_type,
818
1661
  instance_type: instance_type,
819
1662
  type_vars: type_vars,
820
1663
  block_required: true,