rigortype 0.3.8 → 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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -3,10 +3,13 @@
3
3
  require_relative "../../reflection"
4
4
  require_relative "../../type"
5
5
  require_relative "../../rbs_extended"
6
+ require_relative "../range_constant"
6
7
  require_relative "../rbs_type_translator"
8
+ require_relative "../external_ancestor_resolution"
7
9
  require_relative "../void_origin"
8
10
  require_relative "../optimistic_origin"
9
11
  require_relative "overload_selector"
12
+ require_relative "self_substitute"
10
13
 
11
14
  module Rigor
12
15
  module Inference
@@ -41,14 +44,16 @@ module Rigor
41
44
  #
42
45
  # Remaining limitations:
43
46
  #
44
- # * `block_type:` is ignored; method types that constrain the block return type are not yet honored.
45
- # * Keyword arguments are not threaded through call_arg_types, so overloads with required keywords
46
- # 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.
47
49
  # * Method-level type parameters bind only from two positions: the block return type (Slice 6 phase C)
48
50
  # and a positional parameter whose declared type is EXACTLY a type variable (issue #303 —
49
51
  # `def foo[T]: (T) -> T` binds `T` from the first argument, and carries it into a generic return
50
52
  # such as `-> Array[T]`). A variable reachable only through a container position (`Array[T] arg`),
51
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}).
52
57
  #
53
58
  # See docs/adr/4-type-inference-engine.md for the broader plan.
54
59
  # rubocop:disable-next Metrics/ModuleLength
@@ -64,6 +69,86 @@ module Rigor
64
69
  # objects answer to methods their RBS omits (`ActionController::Base`, `Hash`, …).
65
70
  ALLOWED_RBS_COMPLETE_ANCESTORS = ["Rigor::Plugin::Base"].freeze
66
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
+
67
152
  # Shared empty returns for the argument-position type-variable binding (issue #303). The
68
153
  # no-candidate answer is by far the common case — every non-generic overload takes it — so it must
69
154
  # not allocate.
@@ -71,31 +156,48 @@ module Rigor
71
156
  private_constant :EMPTY_TYPE_VARS
72
157
  EMPTY_TYPE_PARAM_NAMES = [].freeze
73
158
  private_constant :EMPTY_TYPE_PARAM_NAMES
74
-
75
- # @param receiver [Rigor::Type]
76
- # @param method_name [Symbol]
77
- # @param args [Array<Rigor::Type>]
78
- # @param environment [Rigor::Environment]
79
- # @param block_type [Rigor::Type, nil] inferred block return type, propagated from
80
- # `MethodDispatcher.dispatch`. When non-nil, the selector prefers a block-bearing overload and
81
- # binds the method-level type parameter that the block's return type references to `block_type`
82
- # (Slice 6 phase C sub-phase 2).
83
- # @param self_type_override [Rigor::Type, nil] when set, the substitution for `Bases::Self` in the
84
- # method's return type. Used by `MethodDispatcher#try_user_class_fallback` to preserve the
85
- # ORIGINAL receiver as the substitute for `self` even though the dispatch is routed through
86
- # `Nominal[Object]` — so that `Bundler::URI::Generic.dup` (which resolves through the `Object`
87
- # fallback because `Bundler::URI::Generic` lacks RBS) returns `Bundler::URI::Generic` per
88
- # `Kernel#dup: () -> self` rather than `Object`. Defaults to nil (compute self from the resolved
89
- # class_name as before).
90
- # @param public_only [Boolean] when true, a method whose RBS accessibility is `:private` does not
91
- # resolve (the call yields `nil`, i.e. "no rule"). Set by the explicit-non-`self`-receiver
92
- # user-class fallback so a call like `Favourite.select(...)` does not adopt the private
93
- # `Kernel#select` signature.
94
- # @return [Rigor::Type, nil] inferred return type, or `nil` when no rule resolves (no class name,
159
+ NO_BINDING = [nil, nil].freeze
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
171
+
172
+ # Both spellings a resolved `RBS::Types::ClassInstance#name` — or a `Nominal#class_name` built
173
+ # from one — may carry for `Range`; core signatures absolutise, but a plugin-contributed one
174
+ # need not.
175
+ RANGE_TYPE_NAMES = ["::Range", "Range"].freeze
176
+ private_constant :RANGE_TYPE_NAMES
177
+
178
+ # Four fields of `context` shape the answer beyond receiver, method name and arguments.
179
+ #
180
+ # `block_type` is the inferred block return type propagated from `MethodDispatcher.dispatch`; when
181
+ # non-nil, the selector prefers a block-bearing overload and binds the method-level type parameter
182
+ # that the block's return type references to it (Slice 6 phase C sub-phase 2).
183
+ #
184
+ # `self_type_override`, when set, is the substitution for `Bases::Self` in the method's return type.
185
+ # `MethodDispatcher#try_user_class_fallback` uses it to preserve the ORIGINAL receiver as the
186
+ # substitute for `self` even though the dispatch is routed through `Nominal[Object]` — so that
187
+ # `Bundler::URI::Generic.dup` (which resolves through the `Object` fallback because
188
+ # `Bundler::URI::Generic` lacks RBS) returns `Bundler::URI::Generic` per `Kernel#dup: () -> self`
189
+ # rather than `Object`. Nil computes self from the resolved class_name as before.
190
+ #
191
+ # `public_only`, when true, keeps a method whose RBS accessibility is `:private` from resolving (the
192
+ # call yields `nil`, i.e. "no rule"). Set by the explicit-non-`self`-receiver user-class fallback so
193
+ # a call like `Favourite.select(...)` does not adopt the private `Kernel#select` signature.
194
+ #
195
+ # `scope`, when supplied, enables ADR-43 RBS-complete-ancestor resolution against
196
+ # `ALLOWED_RBS_COMPLETE_ANCESTORS`; `nil` keeps inherited calls unresolved (`Dynamic[Top]`) — the
197
+ # FP-safe default for open hierarchies (`< ActionController::Base`, …).
198
+ #
199
+ # @return inferred return type, or `nil` when no rule resolves (no class name,
95
200
  # no method, dispatch on a Top/Dynamic[Top] receiver, etc.).
96
- # @param scope [Rigor::Scope, nil] when supplied, enables ADR-43 RBS-complete-ancestor resolution
97
- # against `ALLOWED_RBS_COMPLETE_ANCESTORS`. `nil` keeps inherited calls unresolved
98
- # (`Dynamic[Top]`) — the FP-safe default for open hierarchies (`< ActionController::Base`, …).
99
201
  def try_dispatch(context)
100
202
  environment = context.environment
101
203
  return nil if environment.nil?
@@ -130,7 +232,7 @@ module Rigor
130
232
  #
131
233
  # This deliberately does NOT differentiate "no overload had a block" from "the block is untyped";
132
234
  # the binder treats both the same way (every parameter defaults to `Dynamic[Top]`).
133
- # @return [Array<Rigor::Type>] positional block parameter types.
235
+ # @return positional block parameter types.
134
236
  def block_param_types(context)
135
237
  environment = context.environment
136
238
  return [] if environment.nil?
@@ -140,7 +242,8 @@ module Rigor
140
242
  receiver: context.receiver,
141
243
  method_name: context.method_name,
142
244
  args: context.args,
143
- environment: environment
245
+ environment: environment,
246
+ scope: context.scope
144
247
  )
145
248
  end
146
249
 
@@ -178,9 +281,19 @@ module Rigor
178
281
  return nil unless descriptor
179
282
 
180
283
  class_name, kind, receiver_args = descriptor
181
- 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)
182
286
  return nil unless method_definition
183
287
  return nil if public_only && method_private?(method_definition)
288
+ # Issue #823 — a declaration that states the member's presence and parameters but not its
289
+ # return (`%a{rigor:v1:inferred-return}`, written by `rigor-rbs-inline` on every type slot it
290
+ # DEFAULTED). Declining here is the whole mechanism: this tier's product IS the return type, so
291
+ # withholding it routes the call down the same tiers an undeclared method takes and the body
292
+ # gets typed. Everything the declaration DOES say survives, because the rules that read it —
293
+ # `call.undefined-method`, `call.wrong-arity`, argument-type checking — look the method up in
294
+ # the environment themselves rather than reading this tier's answer.
295
+ return nil if RbsExtended.inferred_return?(method_definition)
296
+ return SPACESHIP_ENVELOPE if inherited_identity_spaceship?(method_definition, class_name, kind, method_name)
184
297
 
185
298
  type_vars = build_type_vars(environment, class_name, receiver_args)
186
299
  translate_return_type(
@@ -192,7 +305,8 @@ module Rigor
192
305
  type_vars: type_vars,
193
306
  block_type: block_type,
194
307
  environment: environment,
195
- self_type_override: self_type_override,
308
+ self_type_override: self_type_override ||
309
+ SelfSubstitute.for(receiver, receiver_args, method_name, args, block_type),
196
310
  scope: scope,
197
311
  call_node: call_node
198
312
  )
@@ -202,6 +316,46 @@ module Rigor
202
316
  nil
203
317
  end
204
318
 
319
+ # Issue #661 — `Kernel#<=>: (untyped other) -> 0?` is the IDENTITY comparison: `0` when the two
320
+ # are the same object, `nil` otherwise, and correct as written for a bare `Object`. Reached by
321
+ # inheritance it stops being a statement about the call. `1.day <=> 2.days` typed as `0?`, and a
322
+ # `Money` whose signature says `include Comparable` — whose whole contract is that the includer
323
+ # defines `<=>` — typed the same, because neither declares `<=>` of its own and every class
324
+ # inherits Kernel's.
325
+ #
326
+ # `0?` is a VALUE claim, so the cost is not confined to the expression: narrowing
327
+ # `n = a <=> b; n.negative? if n` reads the truthy arm as the literal `0`, folds `0.negative?`
328
+ # to false, and answers `bot` for a branch the runtime takes on every ordered pair. That is one
329
+ # `clause.unreachable` away from a false positive on correct code.
330
+ #
331
+ # Widened rather than declined: `Integer?` is the envelope Ruby's own `<=>` convention states and
332
+ # the one upstream rbs writes wherever a class DOES declare the operator (`Array`, `Module`,
333
+ # `Complex`). It is a supertype of `0?`, so this only ever removes a conclusion — and it removes
334
+ # exactly the conclusions that rested on the receiver not overriding an operator its signature
335
+ # was never required to mention (ADR-5: a partially-declared class is not a closed world).
336
+ #
337
+ # Bounded to the three classes that OWN the identity comparison, where the claim is the truth
338
+ # about the receiver rather than an artifact of inheritance, and to `<=>` alone — the general
339
+ # question of what an inherited Object/Kernel signature may assert about a subclass is much
340
+ # larger, and `to_s` / `hash` / `inspect` do not carry a value-precise return to lose.
341
+ SPACESHIP_IDENTITY_OWNERS = %w[Kernel Object BasicObject].to_set.freeze
342
+ private_constant :SPACESHIP_IDENTITY_OWNERS
343
+
344
+ SPACESHIP_ENVELOPE = Type::Combinator.union(
345
+ Type::Combinator.nominal_of("Integer"),
346
+ Type::Combinator.constant_of(nil)
347
+ ).freeze
348
+ private_constant :SPACESHIP_ENVELOPE
349
+
350
+ def inherited_identity_spaceship?(method_definition, class_name, kind, method_name)
351
+ return false unless method_name == :<=>
352
+ return false unless kind == :instance
353
+ return false if SPACESHIP_IDENTITY_OWNERS.include?(class_name.to_s.delete_prefix("::"))
354
+ return false unless method_definition.respond_to?(:defined_in)
355
+
356
+ SPACESHIP_IDENTITY_OWNERS.include?(method_definition.defined_in.to_s.delete_prefix("::"))
357
+ end
358
+
205
359
  # Maps a Rigor::Type receiver to a `[class_name, kind, type_args]` triple where `kind` is either
206
360
  # `:instance` or `:singleton` and `type_args` carries the receiver's generic instantiation (empty
207
361
  # for raw or singleton receivers, since `Singleton[Foo]` carries no generic args today). Returns
@@ -237,6 +391,13 @@ module Rigor
237
391
  # whole dispatch to Dynamic). The refinement-aware promotions (`String#upcase` →
238
392
  # `uppercase-string`, …) run in their own catalog tier ABOVE this one, so they still win.
239
393
  receiver_descriptor(receiver.base)
394
+ when Type::FloatRange
395
+ # ADR-109 WD4 — a bounded Float is a Float for every method the fold tiers do not own.
396
+ ["Float", :instance, []]
397
+ when Type::IntegerRange
398
+ # #842 — a bounded Integer is an Integer for every method the fold tiers
399
+ # (ConstantFolding, ShapeDispatch#dispatch_integer_range) do not own.
400
+ ["Integer", :instance, []]
240
401
  when Type::Dynamic
241
402
  receiver_descriptor(receiver.static_facet)
242
403
  end
@@ -261,11 +422,19 @@ module Rigor
261
422
  [Type::Combinator.union(*tuple.elements)]
262
423
  end
263
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.
264
429
  def hash_shape_type_args(shape)
265
430
  return [] if shape.pairs.empty?
266
431
 
267
432
  key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
268
433
  value_types = shape.pairs.values
434
+ if shape.open?
435
+ key_types += [Type::Combinator.untyped]
436
+ value_types += [Type::Combinator.untyped]
437
+ end
269
438
  [
270
439
  Type::Combinator.union(*key_types),
271
440
  Type::Combinator.union(*value_types)
@@ -280,20 +449,43 @@ module Rigor
280
449
  method_definition.accessibility == :private
281
450
  end
282
451
 
283
- 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)
284
453
  direct = lookup_method_on(environment, class_name, kind, method_name)
285
454
  return direct if direct
286
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
+
287
464
  # ADR-43 — scoped inherited-method resolution. The direct lookup misses when `class_name` is a
288
465
  # Ruby-source subclass absent from RBS (so no ancestor walk runs). If its discovered
289
466
  # superclass chain reaches an allow-listed RBS-complete ancestor, resolve the method there so
290
467
  # inherited contract calls (`self.manifest` on a plugin) resolve and the normal call rules
291
468
  # apply. Bounded to the allow-list, so open hierarchies stay on the Dynamic fallback (no false
292
469
  # positive on `< ActionController::Base`).
293
- ancestor = allowed_rbs_complete_ancestor(environment, class_name, scope)
294
- 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
295
482
 
296
- 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)
297
489
  end
298
490
 
299
491
  def lookup_method_on(environment, class_name, kind, method_name)
@@ -310,36 +502,478 @@ module Rigor
310
502
  # `class_name` is itself RBS-known (the direct lookup already had authority), or when the
311
503
  # discovered chain reaches no allow-listed class. The walk carries a visited set so a malformed
312
504
  # cyclic `A < B < A` source cannot loop.
313
- 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)
314
510
  return nil if scope.nil?
315
511
  return nil if Rigor::Reflection.rbs_class_known?(class_name, environment: environment)
316
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)
317
845
  supers = scope.discovered_superclasses
846
+ includes = scope.discovered_includes
847
+ queue = [class_name.to_s]
318
848
  seen = {}
319
- current = supers[class_name.to_s]
320
- until current.nil? || seen[current]
321
- return current if ALLOWED_RBS_COMPLETE_ANCESTORS.include?(current)
849
+ until queue.empty?
850
+ current = queue.shift
851
+ next if current.nil? || seen[current]
322
852
 
323
853
  seen[current] = true
324
- current = supers[current]
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
+
880
+ supers = scope.discovered_superclasses
881
+ extends = scope.discovered_extends
882
+ queue = [class_name.to_s]
883
+ seen = {}
884
+ until queue.empty?
885
+ current = queue.shift
886
+ next if current.nil? || seen[current]
887
+
888
+ seen[current] = true
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
325
926
  end
326
927
  nil
327
928
  end
328
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
+
329
949
  # Slice 4 phase 2d substitution map. Zips the class's declared type-parameter names against the
330
- # receiver's `type_args`. Returns an empty hash when either side is empty or when arities
331
- # disagree -- in both cases free variables in the method's return type degrade to `Dynamic[Top]`
332
- # 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.
333
962
  def build_type_vars(environment, class_name, receiver_args)
334
- return {} if receiver_args.empty?
963
+ return NO_TYPE_VARS if receiver_args.empty?
335
964
 
336
965
  param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
337
- return {} if param_names.empty?
338
- return {} if param_names.size != receiver_args.size
966
+ return NO_TYPE_VARS if param_names.empty?
967
+ return NO_TYPE_VARS if receiver_args.size > param_names.size
339
968
 
340
- param_names.zip(receiver_args).to_h
969
+ param_names.first(receiver_args.size).zip(receiver_args).to_h
341
970
  end
342
971
 
972
+ # The shared empty substitution map: most receivers carry no type arguments, and the translator
973
+ # only ever reads the map (#775).
974
+ NO_TYPE_VARS = {}.freeze
975
+ private_constant :NO_TYPE_VARS
976
+
343
977
  # rubocop:disable Metrics/ParameterLists
344
978
  def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
345
979
  method_name: nil, environment: nil, self_type_override: nil,
@@ -359,13 +993,16 @@ module Rigor
359
993
  end
360
994
  # `self_type_override` lets the user-class fallback path preserve the ORIGINAL receiver as the
361
995
  # substitute for `Bases::Self` — so `Kernel#dup: () -> self` resolved through the Object
362
- # 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).
363
998
  self_type = self_type_override || resolved_self_type
364
999
 
365
1000
  candidates = OverloadSelector.select_candidates(
366
1001
  method_definition,
367
1002
  arg_types: args,
368
- 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,
369
1006
  instance_type: instance_type,
370
1007
  type_vars: type_vars,
371
1008
  block_required: !block_type.nil?,
@@ -377,6 +1014,7 @@ module Rigor
377
1014
  record_dispatch_provenance(method_definition, candidates.first, scope, call_node, call_site)
378
1015
  join_candidate_returns(
379
1016
  candidates,
1017
+ method_definition: method_definition,
380
1018
  self_type: self_type, instance_type: instance_type, type_vars: type_vars,
381
1019
  args: args, block_type: block_type, scope: scope, call_node: call_node, call_site: call_site,
382
1020
  alias_expander: environment.rbs_loader
@@ -416,17 +1054,20 @@ module Rigor
416
1054
  # A candidate whose return does not translate leaves the join incomplete — decline (fail-soft
417
1055
  # to Dynamic downstream) rather than answer a join missing an arm the runtime can take.
418
1056
  # rubocop:disable-next Metrics/ParameterLists
419
- def join_candidate_returns(candidates, self_type:, instance_type:, type_vars:, args:, block_type:,
420
- 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)
421
1059
  returns = candidates.map do |method_type|
422
1060
  full_type_vars = compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
423
- RbsTypeTranslator.translate(
1061
+ returned = RbsTypeTranslator.translate(
424
1062
  method_type.type.return_type,
425
1063
  self_type: self_type,
426
1064
  instance_type: instance_type,
427
1065
  type_vars: full_type_vars,
428
1066
  alias_expander: alias_expander
429
1067
  )
1068
+ next returned unless combining_overload?(method_definition, method_type, call_site)
1069
+
1070
+ class_level_sum(returned, method_type, args)
430
1071
  end
431
1072
  return returns.first if returns.size == 1
432
1073
  return nil if returns.any?(&:nil?)
@@ -438,11 +1079,100 @@ module Rigor
438
1079
  end
439
1080
 
440
1081
  def compose_type_vars(method_type, type_vars, args, block_type, scope, call_node, call_site)
441
- 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)
442
1084
  compose_arg_type_vars(method_type, vars, args, scope: scope, call_node: call_node,
443
1085
  call_site: call_site)
444
1086
  end
445
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
+
446
1176
  # Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
447
1177
  # `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
448
1178
  # kind]` triple the {VoidOrigin} carries.
@@ -489,13 +1219,160 @@ module Rigor
489
1219
  # block return type at the same call site. Anything outside this exact shape (no block clause,
490
1220
  # an `untyped` block, a non-variable block return type, a variable not declared in
491
1221
  # `type_params`) returns the original `type_vars` so fallbacks stay consistent.
492
- 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:)
493
1236
  return type_vars if block_type.nil?
494
1237
 
495
1238
  block_var_name = method_type_block_return_variable(method_type)
496
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)
497
1242
 
498
- type_vars.merge(block_var_name => block_type)
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
1247
+
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) }
499
1376
  end
500
1377
 
501
1378
  # Issue #303 — bind method-level type parameters from ARGUMENT positions, layering on top of the
@@ -505,7 +1382,8 @@ module Rigor
505
1382
  # Envelope, deliberately the narrowest sound shape:
506
1383
  #
507
1384
  # * only a positional parameter (required or optional) whose declared type is EXACTLY
508
- # `RBS::Types::Variable` — no container walk, so `(Array[T]) -> T` still degrades;
1385
+ # `RBS::Types::Variable`, plus the one container shape issue #834 admits (see
1386
+ # {#range_element_binding}) — no general container walk, so `(Array[T]) -> T` still degrades;
509
1387
  # * only names the SELECTED overload declares in its own `type_params` (a class-level variable
510
1388
  # keeps its receiver-derived binding);
511
1389
  # * an existing key wins, so the receiver and the block return type both outrank an argument;
@@ -534,14 +1412,42 @@ module Rigor
534
1412
  positionals.zip(args).each_with_object({}) do |(param, arg), bindings|
535
1413
  next if arg.nil?
536
1414
 
537
- name = variable_param_name(param, declared, type_vars)
1415
+ name, bound = param_binding(param, arg, declared, type_vars)
538
1416
  next if name.nil?
539
- next if no_static_evidence?(arg)
540
1417
 
541
- bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], arg) : arg
1418
+ bound = Type::Combinator.widen_value_pinned(bound) if upper_bounded?(method_type, name)
1419
+ bindings[name] = bindings.key?(name) ? Type::Combinator.union(bindings[name], bound) : bound
1420
+ end
1421
+ end
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)
542
1434
  end
543
1435
  end
544
1436
 
1437
+ # The `(variable name, bound type)` a positional parameter contributes, or {NO_BINDING} when it
1438
+ # contributes none. The bare-variable shape is tried first because it is the overwhelmingly
1439
+ # common one; the `Range[A]` shape is reached only when the parameter is not a bare variable.
1440
+ def param_binding(param, arg, declared, type_vars)
1441
+ name = variable_param_name(param, declared, type_vars)
1442
+ unless name.nil?
1443
+ return NO_BINDING if no_static_evidence?(arg)
1444
+
1445
+ return [name, arg]
1446
+ end
1447
+
1448
+ range_element_binding(param, arg, declared, type_vars)
1449
+ end
1450
+
545
1451
  def declared_type_param_names(method_type)
546
1452
  params = method_type.respond_to?(:type_params) ? method_type.type_params : nil
547
1453
  return EMPTY_TYPE_PARAM_NAMES if params.nil? || params.empty?
@@ -562,6 +1468,57 @@ module Rigor
562
1468
  name
563
1469
  end
564
1470
 
1471
+ # Issues #834 / #862 — the one container position the envelope admits: a `Range[A]` parameter
1472
+ # against a Range argument that names its own element. `Comparable#clamp: [A] (Range[A]) ->
1473
+ # (self | A)` otherwise leaves `A` unbound and `i.clamp(1..9)` answers `Dynamic[top] | Integer`,
1474
+ # even though the argument names the element outright. It stays Range-only because the
1475
+ # justification does not generalise: a Range is immutable and its element type is fixed at
1476
+ # construction, whereas an `Array[T]` argument's carrier may have been widened long before the
1477
+ # call reached here.
1478
+ def range_element_binding(param, arg, declared, type_vars)
1479
+ declared_type = param.type
1480
+ return NO_BINDING unless declared_type.is_a?(RBS::Types::ClassInstance)
1481
+ return NO_BINDING unless RANGE_TYPE_NAMES.include?(declared_type.name.to_s)
1482
+ return NO_BINDING unless declared_type.args.size == 1
1483
+
1484
+ element = declared_type.args.first
1485
+ return NO_BINDING unless element.is_a?(RBS::Types::Variable)
1486
+ return NO_BINDING unless declared.include?(element.name)
1487
+ return NO_BINDING if type_vars.key?(element.name)
1488
+
1489
+ bound = range_argument_element(arg)
1490
+ bound.nil? ? NO_BINDING : [element.name, bound]
1491
+ end
1492
+
1493
+ # The element a Range argument names, or nil when it names none.
1494
+ #
1495
+ # Two carriers qualify. A `Constant<Range>` contributes its endpoints lifted to their classes
1496
+ # ({RangeConstant.element_type}), not the two values, so `clamp(1..9)` answers `Integer` rather
1497
+ # than a `1 | 9` the runtime contradicts for every receiver already inside the bracket. A
1498
+ # `Nominal[Range, [T]]` (issue #862 — `1..ARGV.size` has no literal endpoint to read, so
1499
+ # `ExpressionTyper` hands back the nominal carrier) contributes `T` itself.
1500
+ #
1501
+ # A nominal carrier qualifies only when `T` is a Nominal or a union of Nominals. `untyped`, a
1502
+ # `Dynamic`, or a still-unbound type variable would launder an unknown into the result — the
1503
+ # caller's `self | A` would read as `self | unknown` while claiming to be inferred — so those
1504
+ # keep degrading as an unbound variable does.
1505
+ def range_argument_element(arg)
1506
+ return RangeConstant.element_type(arg) unless arg.is_a?(Type::Nominal)
1507
+ return nil unless RANGE_TYPE_NAMES.include?(arg.class_name)
1508
+ return nil unless arg.type_args.size == 1
1509
+
1510
+ element = arg.type_args.first
1511
+ nominal_only?(element) ? element : nil
1512
+ end
1513
+
1514
+ def nominal_only?(type)
1515
+ case type
1516
+ when Type::Nominal then true
1517
+ when Type::Union then type.members.all?(Type::Nominal)
1518
+ else false
1519
+ end
1520
+ end
1521
+
565
1522
  # `Dynamic[top]` is the engine's "we could not tell" answer, so binding a variable to it would
566
1523
  # dress up an absence of evidence as an inference. The variable stays unbound and degrades as it
567
1524
  # did before, which is the same value anyway.
@@ -625,11 +1582,11 @@ module Rigor
625
1582
 
626
1583
  # ----- block parameter probe (Phase C sub-phase 1) -----
627
1584
 
628
- def probe_block_param_types(receiver:, method_name:, args:, environment:)
1585
+ def probe_block_param_types(receiver:, method_name:, args:, environment:, scope: nil)
629
1586
  args ||= []
630
1587
  case receiver
631
- when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment)
632
- 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)
633
1590
  end
634
1591
  end
635
1592
 
@@ -637,9 +1594,9 @@ module Rigor
637
1594
  # member resolves the same arity and types (otherwise the call sites would have to thread
638
1595
  # per-member binders, which the slice does not support yet). Mismatches degrade to the empty
639
1596
  # array so the binder defaults all params to Dynamic[Top].
640
- def probe_block_param_types_union(receiver, method_name, args, environment)
1597
+ def probe_block_param_types_union(receiver, method_name, args, environment, scope)
641
1598
  results = receiver.members.map do |member|
642
- probe_block_param_types_one(member, method_name, args, environment)
1599
+ probe_block_param_types_one(member, method_name, args, environment, scope)
643
1600
  end
644
1601
  return [] if results.empty?
645
1602
  return [] unless results.all? { |r| r == results.first }
@@ -647,12 +1604,12 @@ module Rigor
647
1604
  results.first
648
1605
  end
649
1606
 
650
- def probe_block_param_types_one(receiver, method_name, args, environment)
1607
+ def probe_block_param_types_one(receiver, method_name, args, environment, scope)
651
1608
  descriptor = receiver_descriptor(receiver)
652
1609
  return [] unless descriptor
653
1610
 
654
1611
  class_name, kind, receiver_args = descriptor
655
- method_definition = lookup_method(environment, class_name, kind, method_name)
1612
+ method_definition = lookup_method(environment, class_name, kind, method_name, scope)
656
1613
  return [] unless method_definition
657
1614
 
658
1615
  type_vars = build_type_vars(environment, class_name, receiver_args)
@@ -662,14 +1619,20 @@ module Rigor
662
1619
  kind: kind,
663
1620
  args: args,
664
1621
  type_vars: type_vars,
665
- environment: environment
1622
+ environment: environment,
1623
+ receiver: receiver,
1624
+ receiver_args: receiver_args,
1625
+ method_name: method_name
666
1626
  )
667
1627
  rescue StandardError
668
1628
  []
669
1629
  end
670
1630
 
1631
+ # rubocop:disable Metrics/ParameterLists
671
1632
  def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
672
- environment: nil)
1633
+ environment: nil, receiver: nil, receiver_args: [],
1634
+ method_name: nil)
1635
+ # rubocop:enable Metrics/ParameterLists
673
1636
  instance_type = Type::Combinator.nominal_of(class_name)
674
1637
  self_type =
675
1638
  case kind
@@ -677,10 +1640,24 @@ module Rigor
677
1640
  else instance_type
678
1641
  end
679
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
+
680
1655
  method_type = OverloadSelector.select(
681
1656
  method_definition,
682
1657
  arg_types: args,
683
- 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,
684
1661
  instance_type: instance_type,
685
1662
  type_vars: type_vars,
686
1663
  block_required: true,