rigortype 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "../../rbs_extended"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module MethodDispatcher
9
+ # Issue #1350 — how `OverloadSelector` reads a `Dynamic[T]` argument. The internal spec keeps a `Dynamic` whose
10
+ # static facet is not `top` out of the imprecise arguments because its facet discriminates, but acceptance
11
+ # short-circuits on the wrapper and takes it against any parameter, so the receiver-affinity pre-sort's first arm
12
+ # won: `Money#add(Integer(v))` typed `Money` and `.even?` reported `call.undefined-method`.
13
+ #
14
+ # Selection therefore reads a narrow facet's members ({.select}), leaving out `nil`: a facet's `nil` is usually
15
+ # the join of an overload that returns it for some untyped input (`Integer(v)`'s `exception:` form), and chosen
16
+ # by it `Integer | nil` fell through to the only arm that takes `nil` and `Complex(Integer(v), 1)` turned
17
+ # `Complex?`. One member left stands in for the argument, pass 0 included, and the return stays unwrapped; two
18
+ # are selected one by one, and the distinct picks come back together for the dispatch layer's join, wrapped in
19
+ # `Dynamic` when their returns differ (#521).
20
+ #
21
+ # Only sealed members are read ({.sealed?}): a literal, or a plain `Integer`, `Float`, `Rational`, `Complex` or
22
+ # `Symbol`, core classes whose `new` and `allocate` are undefined. Such a member's runtime value is of exactly its
23
+ # class, where any other member's may be of a subclass and take an arm the member itself skips: `Numeric` in
24
+ # `2 ** v`'s `Complex | Numeric` skipped a project `(real) -> Float`, whose `real` alias names `Integer`, for a
25
+ # catch-all `(untyped) -> nil`, and `.floor` on the call reported `call.undefined-method`. Scanning the parameters
26
+ # for a subclass of the member caught a plain class name and missed aliases, `instance`, type variables,
27
+ # intersections, singletons and modules.
28
+ #
29
+ # Even a sealed member is read only where acceptance can rule it out ({.provable?}). Acceptance reads class
30
+ # relations from the analyzer's own process, so it answers `no` for `Integer` against a `(Printable)` that project
31
+ # RBS or source includes into `Integer` (#1352). A plain argument shares that, but the wrapper hid it from a
32
+ # `Dynamic` one: with `(Printable) -> String | (Integer) -> Integer`, reading the member typed
33
+ # `fmt(Integer(v)).upcase` as `Integer` and fired `call.undefined-method` where master read `String`. So every
34
+ # overload's positional parameters must be spelled as a class RBS declares, `untyped`, `top` or `nil`, through `?`
35
+ # and `|`, which leaves acceptance only a sealed class's fixed ancestry to read. A literal or `bool` parameter
36
+ # asks for a value, which a class member cannot prove it holds: `Symbol` skipped `(:json) -> String` for
37
+ # `(untyped) -> nil`, and a `TrueClass` member was refused by `bool` itself. Anything else, a module, a stubbed
38
+ # name, an alias, an interface, a type variable, `instance`, `self`, an intersection, a singleton or an untyped
39
+ # `(?)`, keeps the wrapper too. The list names what is provable rather than what is not, so a form it has not met
40
+ # falls back to master's reading.
41
+ #
42
+ # A wider facet keeps the wrapper and the receiver's arm, as before. It is usually itself a #521 join
43
+ # (`Dynamic[BigDecimal | Complex | Float | Integer | Rational]` from `n * untyped`), and read member by member it
44
+ # joined every arm again: 256 call sites across the survey corpus lost a precise type to `Dynamic`, and a
45
+ # recursion's fuel-exhausted `n * of(n - 1)` stopped reading `Integer`.
46
+ module FacetDistribution
47
+ # The most facet members, `nil` aside, that selection reads one by one.
48
+ MEMBER_LIMIT = 2
49
+ # Member-wise argument lists beyond this keep their wrappers.
50
+ CAP = 8
51
+ # Core classes whose `new` and `allocate` are undefined, so plain Ruby cannot make an instance of a subclass
52
+ # (`Integer`, `Float` and `Symbol` have no allocator at all; a `Rational` or `Complex` subclass instance takes
53
+ # `Marshal.load` or a rebound `Class#allocate`). `NilClass` leaves with the facet's `nil`, and `true` and
54
+ # `false` are read only as literals.
55
+ SEALED_CLASSES = %w[Integer Float Rational Complex Symbol].freeze
56
+
57
+ module_function
58
+
59
+ # Whether any argument is a `Dynamic` whose facet selection reads; the cheap guard of the hot path.
60
+ def faceted?(arg_types) = arg_types.any? { |arg| arg.is_a?(Type::Dynamic) && facet_members(arg) }
61
+
62
+ # Selects through the block, called as `yield(arg_types, member)`. It yields once per member-wise list, one
63
+ # member standing in for each faceted argument, with `member` true so the block answers only a genuine match.
64
+ # It yields once with the arguments as given instead when the overloads are not {.provable?}, when an argument
65
+ # has two members and the caller is the singular `select` (`member_wise` false, whose one answer a member order
66
+ # would decide), when there are more than {CAP} lists, or when any list matches nothing.
67
+ def select(arg_types, definition, member_wise:, environment:, &)
68
+ choices = arg_types.map { |arg| facet_members(arg) || [arg] }
69
+ return yield(arg_types, false) unless member_wise || choices.all? { |members| members.size == 1 }
70
+ return yield(arg_types, false) unless provable?(definition, environment)
71
+
72
+ picks = picks_by_member(choices, &)
73
+ picks.empty? ? yield(arg_types, false) : picks
74
+ end
75
+
76
+ # The distinct overloads the member-wise lists pick; none past {CAP} or when any list picks nothing.
77
+ def picks_by_member(choices)
78
+ return [] if choices.reduce(1) { |count, members| count * members.size } > CAP
79
+
80
+ lists = choices.first.product(*choices.drop(1)).map { |combination| yield(combination, true) }
81
+ lists.any?(&:empty?) ? [] : lists.flatten(1).uniq(&:object_id)
82
+ end
83
+
84
+ # Whether every overload's positional parameters, rest and trailing included, are in a provable form
85
+ # ({.provable_param?}), where acceptance's answer for a sealed member rests only on the member class's fixed
86
+ # ancestry; a `rigor:v1:param:` override proves nothing either.
87
+ def provable?(definition, environment)
88
+ loader = environment&.rbs_loader
89
+ return false if loader.nil?
90
+ return false unless RbsExtended.param_type_override_map(definition, environment: environment).empty?
91
+
92
+ definition.method_types.all? do |method_type|
93
+ fun = method_type.type
94
+ next false unless fun.respond_to?(:required_positionals)
95
+
96
+ params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
97
+ params += [fun.rest_positionals] if fun.rest_positionals
98
+ params.all? { |param| provable_param?(param.type, loader) }
99
+ end
100
+ end
101
+
102
+ def provable_param?(rbs_type, loader)
103
+ case rbs_type
104
+ when RBS::Types::ClassInstance then declared_class?(rbs_type.name.to_s.delete_prefix("::"), loader)
105
+ when RBS::Types::Optional then provable_param?(rbs_type.type, loader)
106
+ when RBS::Types::Union then rbs_type.types.all? { |member| provable_param?(member, loader) }
107
+ when RBS::Types::Bases::Any, RBS::Types::Bases::Top, RBS::Types::Bases::Nil then true
108
+ else false
109
+ end
110
+ end
111
+
112
+ # A class RBS declares, not a module and not a name Rigor stubbed because no RBS declares it: a class cannot be
113
+ # included, and a sealed member's class has a fixed ancestry, so acceptance's answer against it holds.
114
+ def declared_class?(name, loader)
115
+ loader.class_known?(name) && !loader.rbs_module?(name) && !loader.synthesized_type_names.include?(name)
116
+ end
117
+
118
+ # A `Dynamic` argument's facet members other than `nil`, or nil when selection keeps the wrapper: not a
119
+ # `Dynamic`, the untyped carrier, a facet that is only `nil`, one wider than {MEMBER_LIMIT}, or one with a
120
+ # member that is not sealed, an untyped member included.
121
+ def facet_members(arg)
122
+ return nil unless arg.is_a?(Type::Dynamic) && !arg.static_facet.is_a?(Type::Top)
123
+
124
+ facet = arg.static_facet
125
+ members = (facet.is_a?(Type::Union) ? facet.members : [facet]).reject { |member| nil_value?(member) }
126
+ members if members.size.between?(1, MEMBER_LIMIT) && members.all? { |member| sealed?(member) }
127
+ end
128
+
129
+ # A member whose runtime value is of exactly its class: a `Constant`, which is a value of that exact class, or a
130
+ # plain instance of {SEALED_CLASSES}.
131
+ def sealed?(member)
132
+ case member
133
+ when Type::Constant then true
134
+ when Type::Nominal then member.type_args.empty? && SEALED_CLASSES.include?(member.class_name)
135
+ else false
136
+ end
137
+ end
138
+
139
+ def nil_value?(type)
140
+ return type.value.nil? if type.is_a?(Type::Constant)
141
+
142
+ type.is_a?(Type::Nominal) && type.class_name == "NilClass"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # `Hash#transform_keys(mapping)` return type, with or without a block.
9
+ #
10
+ # CRuby's `transform_keys_hash_i` looks each old key up in the mapping with `rb_hash_lookup2`, which
11
+ # ignores a default. A hit takes the mapping's value as the new key and never reaches the block. A miss
12
+ # yields the old key to the block, or keeps it when no block is given. The new key type is therefore the
13
+ # mapping's value type joined with the block's return type, or with the receiver's key type when there is
14
+ # no block. The values are the receiver's, untouched.
15
+ #
16
+ # Neither RBS line says that. rbs 4.2 declares `[K2] (hash[_Key, K2] replacements) { (K old_key) -> K2 }
17
+ # -> Hash[K2, V]`, and {RbsDispatch} leaves `K2` untyped because the mapping and the block both decide it.
18
+ # The blockless `[K2] (hash[_Key, K2]) -> Hash[K | K2, V]` leaves `K2` unbound too. rbs 3.x declares no
19
+ # mapping overload at all; `data/core_overlay/hash_rbs3.rbs` supplies rbs 4.2's on that line, with the same gaps.
20
+ # This tier answers ahead of both, so the forms it accepts get one answer on either RBS line.
21
+ #
22
+ # A mapping the analysis cannot read contributes a `Dynamic[top]` key arm instead of nothing. That covers an
23
+ # untyped argument, a `to_hash`-convertible object, a Hash subclass, the unlisted entries of an open shape,
24
+ # and an empty closed shape, which is what a mapping filled through an alias the engine does not track still
25
+ # reads as. A block the call carries but the block pass could not type contributes `Dynamic[top]` too. A
26
+ # literal with a `**splat` entry needs no arm of its own: its type joins the splatted hash's pairs and a
27
+ # `Dynamic[top]` arm, which also keeps a copy the code writes into (`m = { **o }; m[:b] = :w`) growing. A
28
+ # non-empty shape filled through an alias still reads narrower than it is: that is a gap in the binding's
29
+ # own type.
30
+ #
31
+ # Declines, leaving the RBS answer, when:
32
+ #
33
+ # - the method is not `transform_keys`. `transform_keys!` returns `self` and stays with RBS.
34
+ # - the argument count is not one, or is unknown with no block. A `*splat`, a forwarded `...`, or a lone
35
+ # `**splat` may pass no argument, and with no block that call answers an `Enumerator`. With a block, the
36
+ # call is the block form or the mapping form, and a `Dynamic[top]` mapping arm covers both.
37
+ # - a receiver member is not a `Hash` carrier: a `HashShape` other than the empty closed one, a `Hash`
38
+ # nominal, or a difference over one (`non-empty-hash[K, V]`). A subclass may override the method
39
+ # (`ActiveSupport::HashWithIndifferentAccess` does), and an empty closed shape answers `{}` whatever the
40
+ # mapping says.
41
+ module HashTransformKeysFolding
42
+ module_function
43
+
44
+ def try_dispatch(context)
45
+ return nil unless context.method_name == :transform_keys
46
+
47
+ block = block_arm(context)
48
+ renamed = mapping_arm(context, block)
49
+ return nil if renamed.nil?
50
+
51
+ receiver = context.receiver
52
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
53
+ answers = members.map { |member| answer_for(member, renamed, block) }
54
+ return nil if answers.any?(&:nil?)
55
+
56
+ Type::Combinator.union(*answers)
57
+ end
58
+
59
+ # The mapping form's block parameter: the receiver's key type, as rbs 4.2's `{ (K old_key) -> K2 }`
60
+ # declares it, and as `data/core_overlay/hash_rbs3.rbs` restates it on the rbs 3.x line. The RBS probe
61
+ # does not bind it for a union of receiver shapes. Reached through {IteratorDispatch.block_param_types};
62
+ # nil falls through to the RBS probe.
63
+ def block_param_types(context)
64
+ return nil unless context.args.size == 1
65
+
66
+ receiver = context.receiver
67
+ members = receiver.is_a?(Type::Union) ? receiver.members : [receiver]
68
+ keys = members.map { |member| receiver_key_value(member)&.first }
69
+ return nil if keys.any?(&:nil?)
70
+
71
+ [Type::Combinator.union(*keys)]
72
+ end
73
+
74
+ # `Hash[unmapped | renamed, V]` for one receiver member, or nil when the member is not a `Hash` carrier.
75
+ def answer_for(receiver, renamed, block)
76
+ key_value = receiver_key_value(receiver)
77
+ return nil if key_value.nil?
78
+
79
+ key, value = key_value
80
+ new_key = Type::Combinator.union(block || key, renamed)
81
+ Type::Combinator.nominal_of("Hash", type_args: [new_key, value])
82
+ end
83
+
84
+ # The receiver's `[K, V]`, projected the way {RbsDispatch} projects a shape, or nil when the receiver
85
+ # is not a `Hash` carrier. An open shape may hold entries it does not list, so its key and value take
86
+ # a `Dynamic[top]` arm.
87
+ def receiver_key_value(receiver)
88
+ case receiver
89
+ when Type::HashShape then shape_key_value(receiver)
90
+ when Type::Nominal then nominal_key_value(receiver)
91
+ when Type::Difference then receiver_key_value(receiver.base)
92
+ end
93
+ end
94
+
95
+ def shape_key_value(shape)
96
+ return nil if shape.pairs.empty? && shape.closed?
97
+
98
+ key = Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
99
+ value = Type::Combinator.union(*shape.pairs.values)
100
+ return [key, value] if shape.closed?
101
+
102
+ [Type::Combinator.union(key, unknown), Type::Combinator.union(value, unknown)]
103
+ end
104
+
105
+ def nominal_key_value(nominal)
106
+ return nil unless nominal.class_name == "Hash"
107
+ return [unknown, unknown] unless nominal.type_args.size == 2
108
+
109
+ nominal.type_args
110
+ end
111
+
112
+ # The new keys the mapping supplies, or nil when the call is not one this tier answers (see the module
113
+ # comment for the argument-count rule).
114
+ def mapping_arm(context, block)
115
+ arguments = argument_nodes(context.call_node)
116
+ return block.nil? ? nil : unknown if unresolved_argument_count?(arguments)
117
+ return nil unless context.args.size == 1
118
+
119
+ mapping_value_type(context.args.first)
120
+ end
121
+
122
+ # The mapping's value type. Anything whose value type cannot be read contributes `Dynamic[top]`,
123
+ # including a member that is not a Hash at all (`to_hash` may still convert it, and a `TypeError` for one
124
+ # that cannot is not this tier's to report).
125
+ def mapping_value_type(mapping)
126
+ case mapping
127
+ when Type::HashShape then shape_value_type(mapping)
128
+ when Type::Nominal then nominal_value_type(mapping)
129
+ when Type::Difference then mapping_value_type(mapping.base)
130
+ when Type::Union then Type::Combinator.union(*mapping.members.map { |member| mapping_value_type(member) })
131
+ else unknown
132
+ end
133
+ end
134
+
135
+ # An empty closed shape contributes `Dynamic[top]`, not `bot`: the engine records no aliasing, so
136
+ # `m = {}; m.tap { |x| x[:a] = :z }` still reads `{}` after the store, and a `bot` arm would drop `:z`.
137
+ # A non-empty shape filled through an alias has the same gap, which this rule does not close.
138
+ def shape_value_type(shape)
139
+ return unknown if shape.pairs.empty? && shape.closed?
140
+
141
+ value = Type::Combinator.union(*shape.pairs.values)
142
+ shape.closed? ? value : Type::Combinator.union(value, unknown)
143
+ end
144
+
145
+ def nominal_value_type(nominal)
146
+ return unknown unless nominal.class_name == "Hash" && nominal.type_args.size == 2
147
+
148
+ nominal.type_args[1]
149
+ end
150
+
151
+ # The block's return type; `Dynamic[top]` when the call carries a block the block pass left untyped (it
152
+ # rescues to nil); nil when there is no block, so the unmapped keys keep the receiver's key type. `&nil`
153
+ # passes no block, whatever type the block pass gave it.
154
+ def block_arm(context)
155
+ block_node = context.call_node.is_a?(Prism::CallNode) ? context.call_node.block : nil
156
+ return nil if block_node.is_a?(Prism::BlockArgumentNode) && block_node.expression.is_a?(Prism::NilNode)
157
+ return context.block_type if context.block_type
158
+
159
+ block_node ? unknown : nil
160
+ end
161
+
162
+ # The call's argument nodes, or an empty list for a nil or non-call node (an internal caller, whose
163
+ # argument types are authoritative).
164
+ def argument_nodes(call_node)
165
+ return EMPTY_ARGUMENTS unless call_node.is_a?(Prism::CallNode)
166
+
167
+ call_node.arguments&.arguments || EMPTY_ARGUMENTS
168
+ end
169
+
170
+ EMPTY_ARGUMENTS = [].freeze
171
+ private_constant :EMPTY_ARGUMENTS
172
+
173
+ # A `*splat`, a forwarded `...`, or keyword arguments made only of `**splat` entries: each may pass no
174
+ # argument at all (`transform_keys(**{})` passes none), so whether there is a mapping is not static.
175
+ def unresolved_argument_count?(arguments)
176
+ arguments.any? do |argument|
177
+ case argument
178
+ when Prism::SplatNode, Prism::ForwardingArgumentsNode then true
179
+ when Prism::KeywordHashNode then argument.elements.all?(Prism::AssocSplatNode)
180
+ else false
181
+ end
182
+ end
183
+ end
184
+
185
+ def unknown
186
+ Type::Combinator.untyped
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../type"
4
4
  require_relative "singleton_folding"
5
+ require_relative "hash_transform_keys_folding"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -22,6 +23,8 @@ module Rigor
22
23
  # bound from the argument.
23
24
  # - `a.downto(b) { |i| … }` yields the same domain `[b, a]`, just iterated in reverse. Lower bound
24
25
  # from the argument, upper bound from the receiver.
26
+ # - `h.transform_keys(mapping) { |k| … }` yields the receiver's keys, a rule the RBS probe does not state for
27
+ # a union of receiver shapes ({HashTransformKeysFolding.block_param_types}).
25
28
  module IteratorDispatch
26
29
  module_function
27
30
 
@@ -41,6 +44,7 @@ module Rigor
41
44
  when :group_by, :partition then single_element_block_params(receiver)
42
45
  when :each_slice, :each_cons then slice_block_params(receiver)
43
46
  when :new then class_new_block_params(receiver, args)
47
+ when :transform_keys then HashTransformKeysFolding.block_param_types(context)
44
48
  end
45
49
  end
46
50
 
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "regexp_folding"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module MethodDispatcher
9
+ # Issue #1381 — slice reads of the proven `$~`. On a proven-match edge the flow engine narrows `$~` to
10
+ # `MatchData` and each unconditional group's `$N` to `String` (see `Narrowing#regex_match_predicate_scopes`).
11
+ # The Integer overload of RBS `MatchData#[]` already reads `String`, but the Range and `(start, length)`
12
+ # overloads and `values_at` read `Array[String?]`, and destructuring that binds every slot `String?`.
13
+ # This tier answers those reads with a Tuple, one slot per group, using the rule
14
+ # {RegexpFolding.fold_last_match_group} applies to `Regexp.last_match(N)`: `String` when `$N` is bound,
15
+ # `String?` when it is not (an optional or alternation-reachable group is nil on a successful match).
16
+ # Index 0 is the whole match, always a `String`.
17
+ #
18
+ # Supported, all with compile-time constant Integer arguments:
19
+ #
20
+ # * `$~[a..b]` / `$~[a...b]`
21
+ # * `$~[start, length]`
22
+ # * `$~.values_at(i, j, ...)`
23
+ #
24
+ # The receiver must be the `$~` read itself or a no-argument `Regexp.last_match`: a local MatchData carries
25
+ # no group facts, and the scope's `$N` describe only the frame's current match.
26
+ #
27
+ # The scope does not record the regex's group count, and a slice past the last group is shorter than the
28
+ # range asked for (`$~[1..3]` on a one-group match is `["x"]`) while `values_at` pads with nil. The highest
29
+ # bound `$K` is a lower bound on the count — `Scope#join_bindings` keeps a key only when both sides bind
30
+ # it — so every index must be at most K. Declines (RBS answers) when:
31
+ #
32
+ # - `$~` is not proven non-nil, or no scope is threaded through,
33
+ # - no `$1`..`$9` narrows to `String` (every group optional, no group at all, `when /a/, /b/`),
34
+ # - any index is above K, negative, or not a constant Integer, or a Range bound is nil (endless /
35
+ # beginless),
36
+ # - the slice is empty.
37
+ #
38
+ # K is read from `$1`..`$9` only: `Scope#forget_match_globals` does not clear `$10` and above (#1384), so a
39
+ # binding there may belong to an earlier match.
40
+ module MatchDataFolding
41
+ MAX_GROUP_GLOBAL = 9
42
+ private_constant :MAX_GROUP_GLOBAL
43
+
44
+ module_function
45
+
46
+ # @return folded Tuple, or nil to defer.
47
+ def try_dispatch(context)
48
+ receiver = context.receiver
49
+ return nil unless receiver.is_a?(Type::Nominal) && receiver.class_name == "MatchData"
50
+
51
+ scope = context.scope
52
+ return nil if scope.nil?
53
+ return nil unless last_match_read?(context.call_node, scope)
54
+ return nil unless RegexpFolding.proven_match?(scope.global(:$~))
55
+
56
+ highest = highest_bound_group(scope)
57
+ return nil if highest.nil?
58
+
59
+ indices = slot_indices(context.method_name, context.args, highest)
60
+ return nil if indices.nil?
61
+
62
+ Type::Combinator.tuple_of(*indices.map { |index| slot_type(index, scope) })
63
+ end
64
+
65
+ # The receiver expression reads the frame's current match: `$~`, or `last_match` with no arguments and no block
66
+ # on an expression that types as the core `Regexp` class. A project class that happens to be named `Regexp`
67
+ # (`Foo::Regexp` read as `Regexp` inside `module Foo`) types as its own singleton and does not count.
68
+ def last_match_read?(call_node, scope)
69
+ return false unless call_node.is_a?(Prism::CallNode)
70
+
71
+ receiver = call_node.receiver
72
+ case receiver
73
+ when Prism::GlobalVariableReadNode
74
+ receiver.name == :$~
75
+ when Prism::CallNode
76
+ receiver.name == :last_match && receiver.arguments.nil? && receiver.block.nil? &&
77
+ regexp_class?(receiver.receiver, scope)
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ def regexp_class?(node, scope)
84
+ return false if node.nil?
85
+
86
+ type = scope.type_of(node)
87
+ type.is_a?(Type::Singleton) && type.class_name == "Regexp"
88
+ end
89
+
90
+ # The highest N in 1..9 whose `$N` is narrowed to `String`, or nil when none is.
91
+ def highest_bound_group(scope)
92
+ MAX_GROUP_GLOBAL.downto(1).find { |index| string_global?(scope.global(:"$#{index}")) }
93
+ end
94
+
95
+ def string_global?(type)
96
+ type.is_a?(Type::Nominal) && type.class_name == "String"
97
+ end
98
+
99
+ # The group indices the call reads, in result order, or nil when the call shape is not folded or any index
100
+ # falls outside `0..highest`.
101
+ def slot_indices(method_name, args, highest)
102
+ indices =
103
+ case method_name
104
+ when :[] then index_slice(args)
105
+ when :values_at then values_at_indices(args)
106
+ end
107
+ return nil if indices.nil? || indices.none?
108
+ return nil unless indices.all? { |index| index.between?(0, highest) }
109
+
110
+ indices.to_a
111
+ end
112
+
113
+ # `[range]` or `[start, length]`. A single Integer index is left to RBS, which already reads `String`.
114
+ def index_slice(args)
115
+ case args.size
116
+ when 1 then range_indices(args.first)
117
+ when 2 then start_length_indices(*args)
118
+ end
119
+ end
120
+
121
+ def range_indices(arg)
122
+ range = constant_value(arg)
123
+ return nil unless range.is_a?(Range)
124
+
125
+ first = range.begin
126
+ last = range.end
127
+ return nil unless first.is_a?(Integer) && last.is_a?(Integer)
128
+ return nil if first.negative? || last.negative?
129
+
130
+ range
131
+ end
132
+
133
+ def start_length_indices(start_arg, length_arg)
134
+ start = constant_value(start_arg)
135
+ length = constant_value(length_arg)
136
+ return nil unless start.is_a?(Integer) && length.is_a?(Integer)
137
+ return nil if start.negative? || length.negative?
138
+
139
+ start...(start + length)
140
+ end
141
+
142
+ def values_at_indices(args)
143
+ indices = args.map { |arg| constant_value(arg) }
144
+ indices.all?(Integer) ? indices : nil
145
+ end
146
+
147
+ def constant_value(type)
148
+ type.is_a?(Type::Constant) ? type.value : nil
149
+ end
150
+
151
+ def slot_type(index, scope)
152
+ return Type::Combinator.nominal_of("String") if index.zero?
153
+
154
+ RegexpFolding.fold_last_match_group(Type::Combinator.constant_of(index), scope)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end