rigortype 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -3,28 +3,64 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "optimistic_origin"
7
+ require_relative "method_dispatcher/rbs_dispatch"
6
8
 
7
9
  module Rigor
8
10
  module Inference
9
11
  # Slice 5 phase 2 sub-phase 2 destructuring binder.
10
12
  #
11
- # `Rigor::Inference::MultiTargetBinder` decomposes a tuple-shaped right-hand side type
13
+ # `Rigor::Inference::MultiTargetBinder` decomposes a tuple- or array-shaped right-hand side type
12
14
  # against a Prism multi-target tree and produces a `name -> Rigor::Type` binding map. The
13
- # binder is shared between two surfaces:
15
+ # binder is shared between four surfaces:
14
16
  #
15
17
  # 1. `Rigor::Inference::StatementEvaluator#eval_multi_write` for the statement-level `a, b =
16
18
  # rhs` form (`Prism::MultiWriteNode`).
17
19
  # 2. `Rigor::Inference::BlockParameterBinder` for nested destructuring inside block parameter
18
20
  # lists (`Prism::MultiTargetNode` under `BlockParametersNode#requireds`).
21
+ # 3. `Rigor::Inference::StatementEvaluator#bind_for_index` for `for a, b in pairs`.
22
+ # 4. `Rigor::Inference::ScopeIndexer`'s class-ivar pre-pass, for the ivar targets of `@a, @b = rhs`
23
+ # (issue #1110), which records {Result#ivars} and drops the marks, as it does for `@x = xs.first`,
24
+ # with `soften_slots: false` (see {.bind_marked}).
25
+ # 5. `Rigor::Inference::StatementEvaluator`'s `case/in` pattern binding (issue #1122), through
26
+ # {.decompose_slots}: a pattern binds its names against the same carriers, minus the two
27
+ # statement-only properties that method documents.
19
28
  #
20
29
  # Both Prism nodes share the same `lefts` / `rest` (a `Prism::SplatNode`) / `rights` triple,
21
30
  # so the binder treats them uniformly. The binder is pure: it MUST NOT mutate its inputs and
22
31
  # MUST return a fresh `Hash` on every call.
23
32
  #
24
- # The binder threads `Type::Tuple` decompositions when the right-hand side carrier is a
25
- # known-arity tuple. Other carriers (`Nominal[Array]`, `Dynamic[Top]`, `Top`, `Bot`, ...)
26
- # collapse to `Dynamic[Top]` per slot — Slice 5 phase 2 sub-phase 2 stays conservative on
27
- # dynamic-arity right-hand sides until the narrower receiver-shape lattice lands.
33
+ # Per-carrier rule for the right-hand side:
34
+ #
35
+ # - `Type::Tuple` (known arity) decomposes element-wise; see {slot_type} for the softening.
36
+ # - `Nominal[Array, [T]]`, and a `Refined` / `Difference` over one (`non-empty-array[T]`
37
+ # after an `empty?` guard), binds every fixed front/back slot to `T` and a named rest to
38
+ # `Array[T]` (issue #1093). A short array pads the fixed slots with `nil` at runtime, so
39
+ # `T` is the same optimistic read `Array#[]` / `Array#first` get past core RBS's
40
+ # `%a{implicitly-returns-nil}`: the binder reports those names in {Result#optimistic}, and
41
+ # every caller that applies the bindings to a scope records them through
42
+ # `Scope#with_optimistic_local` ({Result#apply_to}) so the ADR-101 branch elision
43
+ # declines on them. A nested target under an optimistic slot inherits the mark (`nil`
44
+ # destructures to `nil` for every inner name); the rest never carries it.
45
+ # - A `Type::Union` distributes (issue #1094): each member decomposes on its own against the
46
+ # same target tree, and each name binds the join of its per-member types. A member that binds
47
+ # a name to `Dynamic[Top]` — every name, for a member no rule here decomposes — makes that
48
+ # name `Dynamic[Top]` for the whole union rather than dropping out of the join. A member that
49
+ # binds a name to bare `nil` while another binds a value drops out of that name's join, and
50
+ # the name is marked optimistic: the per-slot softening of {slot_type}, applied across
51
+ # members (see {join_member_bindings}). So `a, b = ints[1..]` (`Array[Integer] | nil`) binds
52
+ # `Integer` to each slot, marked, exactly as a short `ints` would. A name is also optimistic
53
+ # when any member marked it, the rule `Scope#join` applies to the same mark at a merge.
54
+ # - A value that provably has no implicit `to_ary` conversion
55
+ # ({MethodDispatcher::RbsDispatch.array_conversion_free?}: `Integer`, `String`, `Hash`,
56
+ # `nil`, ... and RBS-known classes whose ancestry neither defines `to_ary` nor overrides
57
+ # `method_missing` / `respond_to_missing?`) is what Ruby wraps as `[rhs]`, so it decomposes
58
+ # as that one-element `Tuple` (issue #1094): `a, b = 1` binds `1` and `nil`. The wrap is
59
+ # exact rather than a bet, so it adds no mark.
60
+ # - Everything else — raw `Array`, `Array[Dynamic[top]]`, `Dynamic[Array[T]]` (whose static
61
+ # facet must not surface as a bare `T`), a value that may convert (`SimpleDelegator`, a
62
+ # Ruby-source class, a module, `Object`), `Top`, `Bot` — collapses to `Dynamic[Top]` per
63
+ # slot.
28
64
  #
29
65
  # Targets the binder recognises:
30
66
  #
@@ -37,65 +73,304 @@ module Rigor
37
73
  # same observable semantics (binding a fresh local in the block-entry scope).
38
74
  # - `Prism::MultiTargetNode` — recurses with the slot's type as the new right-hand side.
39
75
  # - `Prism::SplatNode` (used for `rest`) — its `expression` MUST be a
40
- # `Prism::LocalVariableTargetNode` or a `Prism::RequiredParameterNode` to be observable;
41
- # an anonymous `*` splat or a non-local target is skipped.
76
+ # `Prism::LocalVariableTargetNode`, a `Prism::RequiredParameterNode`, a
77
+ # `Prism::InstanceVariableTargetNode` or a `Prism::IndexTargetNode` to be observable; an
78
+ # anonymous `*` splat or any other target is skipped.
79
+ # - `Prism::InstanceVariableTargetNode` (issue #1110), as a fixed slot, a rest
80
+ # (`*@rest`), or inside a nested target. It decomposes by the same carrier rules as a
81
+ # local and is reported apart, in {Result#ivars} / {Result#optimistic_ivars}, so a caller
82
+ # that threads only locals never sees it. The binder keys it internally by its `:@name`,
83
+ # which no local name can collide with.
84
+ # - `Prism::IndexTargetNode` (`h[:a], x = rhs`), in the same three positions. It binds no
85
+ # name: it stores its slot through `[]=` on its receiver. The binder reports the value it
86
+ # stores in {Result#index_targets}, keyed by the target node itself, so
87
+ # `StatementEvaluator#eval_multi_write` and `#bind_for_index` (`for h[:a], w in pairs`) can
88
+ # widen the receiver's literal shape with that value as content evidence, exactly as a plain
89
+ # `h[:a] = 1` does. It carries no optimistic mark: there is no binding for one to qualify.
90
+ # Its slot is softened as a local's is; `eval_multi_write` documents why that stays honest
91
+ # without the mark.
42
92
  #
43
- # Other target kinds (`InstanceVariableTargetNode`, `ConstantTargetNode`,
44
- # `IndexTargetNode`, `CallTargetNode`, `ConstantPathTargetNode`, `ImplicitRestNode`, ...)
45
- # MUST be silently skipped: they have no observable contribution to the local-variable scope
46
- # the StatementEvaluator threads.
93
+ # Other target kinds (`ClassVariableTargetNode`, `GlobalVariableTargetNode`,
94
+ # `ConstantTargetNode`, `CallTargetNode`, `ConstantPathTargetNode`, `ImplicitRestNode`, ...)
95
+ # MUST be silently skipped: they have no observable contribution to the scope the
96
+ # StatementEvaluator threads.
47
97
  #
48
98
  # See docs/internal-spec/inference-engine.md for the binding contract and
49
99
  # docs/adr/4-type-inference-engine.md for the slice rationale.
50
100
  module MultiTargetBinder
101
+ NO_BINDINGS = {}.freeze
102
+ NO_NAMES = [].freeze
103
+ private_constant :NO_BINDINGS, :NO_NAMES
104
+
105
+ # `types` is the local `name -> Rigor::Type` map {.bind} returns; `optimistic` the frozen list
106
+ # of local names whose nil-freeness is the short-array bet described in the module comment.
107
+ # `ivars` / `optimistic_ivars` are the same pair for instance-variable targets.
108
+ # `index_targets` maps each `Prism::IndexTargetNode` to the value it stores; {#apply_to}
109
+ # binds nothing for it.
110
+ Result = Data.define(:types, :optimistic, :ivars, :optimistic_ivars, :index_targets) do
111
+ def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, index_targets: NO_BINDINGS)
112
+ super
113
+ end
114
+
115
+ # Binds every name into `scope` with its mark ({MultiTargetBinder.apply_bindings}).
116
+ def apply_to(scope, miss: nil)
117
+ MultiTargetBinder.apply_bindings(scope, types, optimistic, ivars:, optimistic_ivars:, miss:)
118
+ end
119
+ end
120
+
51
121
  module_function
52
122
 
123
+ # The one place bindings and their optimistic marks are applied, shared by {Result#apply_to} and
124
+ # `BlockParameterBinder#bind_onto`. Every name is bound into the scope `bound` and the mark recorded
125
+ # after the binding, since `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried
126
+ # before; the answer is the bound scope. `miss` is what each marked slot answers on a miss (issue
127
+ # #1302): `nil` for every mark the binder makes itself — a short array pads with `nil`, and a `nil`
128
+ # destructures to `nil` — so only a caller whose right-hand side a miss can make something else passes
129
+ # another answer ({OptimisticOrigin.destructuring_miss}). It loops with `each`, since `reduce`
130
+ # allocates even over the empty collections most of the four are
131
+ # ([#1504](https://github.com/rigortype/rigor/issues/1504)).
132
+ def apply_bindings(bound, types, optimistic, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, miss: nil)
133
+ types.each { |name, type| bound = bound.with_local(name, type) }
134
+ ivars.each { |name, type| bound = bound.with_ivar(name, type).without_ivar_guard(name) }
135
+ origin = OptimisticOrigin::IMPLICITLY_RETURNS_NIL
136
+ optimistic.each { |name| bound = bound.with_optimistic_local(name, origin, miss: miss) }
137
+ optimistic_ivars.each { |name| bound = bound.with_optimistic_ivar(name, origin, miss: miss) }
138
+ bound
139
+ end
140
+
53
141
  # @param rhs_type — type of the right-hand side
54
- def bind(target_node, rhs_type)
142
+ # @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
143
+ # question for a class outside the core list; without one only the core list wraps.
144
+ def bind(target_node, rhs_type, scope: nil)
145
+ bind_marked(target_node, rhs_type, scope: scope).types
146
+ end
147
+
148
+ # @param optimistic — whether `rhs_type` itself is an optimistic slot of an enclosing
149
+ # decomposition (a block's `|(g, h)|` fed from an auto-splatted `Array[T]`) or an
150
+ # optimistically nil-free value (`k, v = xs.first`), in which case every name bound under it
151
+ # inherits the mark. It may instead be the per-element Array
152
+ # {OptimisticOrigin.destructuring_marks} builds for a literal right-hand side
153
+ # (`x, y = xs.first, 1`), which marks the names under each fixed slot by that slot's element.
154
+ # @param soften_slots — false keeps a present `X | nil` tuple slot and a union member's bare
155
+ # `nil` in the binding instead of applying the ADR-57 softening of {slot_type} /
156
+ # {join_member_bindings}. That softening is only honest together with the optimistic mark
157
+ # it records, so a consumer that drops the marks (the class-ivar seed, issue #1110) turns it
158
+ # off; the `Array[T]` bet is unaffected, matching what `@x = xs.first` records.
159
+ def bind_marked(target_node, rhs_type, optimistic: false, scope: nil, soften_slots: true)
55
160
  bindings = {}
56
- visit(target_node, rhs_type, bindings)
57
- bindings
161
+ marked = []
162
+ visit(target_node, rhs_type, optimistic, bindings, marked, [scope, soften_slots])
163
+ split_result(bindings, marked)
164
+ end
165
+
166
+ # The per-slot types of a positional pattern (`in [i, s]`, `in [*pre, m, *post]`), sharing
167
+ # every carrier rule {.bind_marked} applies to a multi-write target except two statement-only
168
+ # properties (issue #1122):
169
+ #
170
+ # - the multi-assign `[rhs]` wrap is NOT applied. `a, b = 1` binds `a` to `1` because Ruby wraps
171
+ # a right-hand side with no implicit `to_ary`; an array pattern instead matches through the
172
+ # subject's `deconstruct` / `to_ary`, and a subject with neither raises
173
+ # `NoMatchingPatternError` — no body is reached, so binding `1` there would type dead code.
174
+ # - no optimistic mark is reported. The ADR-101 short-array bet exists because a SHORT
175
+ # right-hand side pads the fixed slots with `nil`; a pattern that matched has every fixed slot
176
+ # it named, so there is nothing to bet on and `Result#optimistic` has no counterpart here.
177
+ #
178
+ # A `Type::Union` is the caller's to distribute: a pattern matches SOME member, and only the
179
+ # caller can tell which members can match it at all. Returns `[fronts, rest_type, backs]`, with
180
+ # every carrier no rule decomposes answering `Dynamic[Top]` per slot — the floor.
181
+ def decompose_slots(rhs_type, front_count:, back_count:, rest_present:, scope: nil, soften_slots: true)
182
+ fronts, rest_type, backs, = decompose(
183
+ rhs_type, front_count, back_count, rest_present: rest_present,
184
+ context: [scope, soften_slots], wrap_single: false
185
+ )
186
+ [fronts, rest_type, backs]
187
+ end
188
+
189
+ # The `T` of an `Array[T]` carrier the binder may decompose, or nil. Declines raw `Array`
190
+ # and an untyped / top element, which bind `Dynamic[Top]` per slot as before, and every
191
+ # `Dynamic` wrapper: `Dynamic[Array[T]]` is gradual, and projecting its static facet would
192
+ # hand the slot a bare `T` that licenses the negative rules (ADR-5). Shared with
193
+ # {BlockParameterBinder}'s auto-splat so both surfaces accept the same carriers.
194
+ def array_element_type(type)
195
+ case type
196
+ when Type::Nominal
197
+ return nil unless type.class_name == "Array" && type.type_args.size == 1
198
+
199
+ element = type.type_args.first
200
+ element.is_a?(Type::Dynamic) || element.is_a?(Type::Top) ? nil : element
201
+ when Type::Refined, Type::Difference
202
+ array_element_type(type.base)
203
+ end
204
+ end
205
+
206
+ # The class whose instances `type` describes, for the `to_ary` question, or nil when `type`
207
+ # names no single class: `Dynamic`, `Top`, `Singleton`, `Intersection` and the rest decline.
208
+ # A union never reaches here; the binder distributes it first.
209
+ def conversion_class_name(type)
210
+ case type
211
+ when Type::Constant then type.value.class.name
212
+ when Type::Nominal then type.class_name
213
+ when Type::HashShape then "Hash"
214
+ when Type::IntegerRange then "Integer"
215
+ when Type::Refined, Type::Difference then conversion_class_name(type.base)
216
+ end
58
217
  end
59
218
 
60
219
  class << self
61
220
  private
62
221
 
63
- def visit(node, rhs_type, bindings)
222
+ # Partitions the walk's single map: an ivar name carries its `@` sigil, which a local name
223
+ # never does, and an index target is keyed by its node rather than by a name.
224
+ def split_result(bindings, marked)
225
+ return Result.new(types: bindings, optimistic: marked.freeze) if bindings.each_key.all? { |k| local_key?(k) }
226
+
227
+ grouped = bindings.group_by { |key, _| key_kind(key) }.transform_values(&:to_h)
228
+ ivar_marked, local_marked = marked.partition { |name| key_kind(name) == :ivar }
229
+ Result.new(types: grouped.fetch(:local, {}), optimistic: local_marked.freeze,
230
+ ivars: grouped.fetch(:ivar, NO_BINDINGS), optimistic_ivars: ivar_marked.freeze,
231
+ index_targets: grouped.fetch(:index, NO_BINDINGS))
232
+ end
233
+
234
+ def local_key?(key) = key.is_a?(Symbol) && !key.start_with?("@")
235
+
236
+ def key_kind(key)
237
+ return :index unless key.is_a?(Symbol)
238
+
239
+ key.start_with?("@") ? :ivar : :local
240
+ end
241
+
242
+ # `context` is the `[scope, soften_slots]` pair every step of the walk shares.
243
+ def visit(node, rhs_type, optimistic, bindings, marked, context)
244
+ if rhs_type.is_a?(Type::Union)
245
+ return visit_union(node, rhs_type.members, optimistic, bindings, marked, context)
246
+ end
247
+
64
248
  lefts = node.lefts || []
65
249
  rest = node.rest
66
250
  rights = node.rights || []
251
+ rest_present = !rest.nil?
67
252
 
68
- fronts, rest_type, backs = decompose(rhs_type, lefts.size, rights.size, rest_present: !rest.nil?)
69
- lefts.each_with_index { |t, i| bind_target(t, fronts[i], bindings) }
70
- bind_rest_target(rest, rest_type, bindings) if rest
71
- rights.each_with_index { |t, i| bind_target(t, backs[i], bindings) }
253
+ fronts, rest_type, backs, slots_optimistic =
254
+ decompose(rhs_type, lefts.size, rights.size, rest_present: rest_present, context: context)
255
+ rest_type = arity_free_rest(rest_type) if optimistic == true
256
+ front_marks, back_marks =
257
+ slot_marks(optimistic, slots_optimistic, rhs_type, [lefts.size, rights.size], rest_present: rest_present)
258
+ lefts.each_with_index { |t, i| bind_target(t, fronts[i], front_marks[i], bindings, marked, context) }
259
+ bind_rest_target(rest, rest_type, bindings, marked) if rest
260
+ rights.each_with_index { |t, i| bind_target(t, backs[i], back_marks[i], bindings, marked, context) }
261
+ end
262
+
263
+ # The `[front_marks, back_marks]` pair a visit hands its fixed slots. A boolean `optimistic`
264
+ # applies to every slot, as does the decomposition's own short-array bet (`fallback`, the
265
+ # issue #1093 `Array[T]` mark). A per-element Array applies only to the `Type::Tuple` a literal
266
+ # right-hand side types as, of the same arity, and each slot picks its element at the offset
267
+ # {decompose_tuple} reads ({slot_offsets}); any other carrier falls back to `fallback`, and a
268
+ # slot past the literal's end binds an exact `nil` that no mark qualifies.
269
+ def slot_marks(optimistic, fallback, rhs_type, (front_count, back_count), rest_present:)
270
+ literal = optimistic.is_a?(Array) && rhs_type.is_a?(Type::Tuple) && rhs_type.elements.size == optimistic.size
271
+ uniform = optimistic.is_a?(Array) ? fallback : optimistic || fallback
272
+ return [[uniform] * front_count, [uniform] * back_count] unless literal
273
+
274
+ slot_offsets(optimistic.size, front_count, back_count, rest_present: rest_present)
275
+ .map { |offsets| offsets.map { |i| optimistic.fetch(i, false) } }
276
+ end
277
+
278
+ # Every member walks the same target tree, so each binds the same names; the first member's
279
+ # key order is the declaration order.
280
+ def visit_union(node, members, optimistic, bindings, marked, context)
281
+ walks = members.map do |member|
282
+ member_bindings = {}
283
+ member_marked = []
284
+ visit(node, member, optimistic, member_bindings, member_marked, context)
285
+ [member_bindings, member_marked]
286
+ end
287
+ walks.first.first.each_key do |name|
288
+ types = walks.map { |member_bindings, _| member_bindings[name] }
289
+ joined, softened = join_member_bindings(types, context.last)
290
+ mark = softened || walks.any? { |_, member_marked| member_marked.include?(name) }
291
+ bind_name(name, joined, mark, bindings, marked)
292
+ end
293
+ end
294
+
295
+ # Returns `[type, softened]`. `Dynamic[Top]` from any member is the whole answer: the join
296
+ # must not let a decomposable member's precise type stand for a member nothing is known
297
+ # about. A member that binds the name to bare `nil` — a `nil` slot, a slot past a short
298
+ # member, or a `nil` member wrapped as `[nil]` — is left out of the join when another member
299
+ # binds a value, and `softened` reports it so the caller marks the name optimistic. This is
300
+ # {slot_type}'s ADR-57 softening across members: which member arrived is correlated with the
301
+ # other slots (`k, v = hash.find { ... }; v.x if k`, `status, value = ok ? [:ok, v] : [:err]`),
302
+ # and a per-slot `T | nil` fires `call.possible-nil-receiver` on the guarded read.
303
+ def join_member_bindings(types, soften_slots)
304
+ untyped = Type::Combinator.untyped
305
+ return [untyped, false] if types.any? { |type| type == untyped }
306
+
307
+ firm = types.reject { |type| nil_literal?(type) }
308
+ return [Type::Combinator.union(*types), false] if !soften_slots || firm.empty? || firm.size == types.size
309
+
310
+ [Type::Combinator.union(*firm), true]
72
311
  end
73
312
 
74
313
  # Decomposes the right-hand side type into the per-slot types. Returns a `[fronts,
75
- # rest_type, backs]` triple, with `fronts` and `backs` each an ordered array of length
76
- # `front_count`/`back_count`, and `rest_type` either a `Rigor::Type` (when
77
- # `rest_present:` is true) or `nil`.
78
- def decompose(rhs_type, front_count, back_count, rest_present:)
314
+ # rest_type, backs, optimistic]` quadruple, with `fronts` and `backs` each an ordered
315
+ # array of length `front_count`/`back_count`, `rest_type` either a `Rigor::Type` (when
316
+ # `rest_present:` is true) or `nil`, and `optimistic` true when the fixed slots are the
317
+ # short-array bet rather than known elements.
318
+ def decompose(rhs_type, front_count, back_count, rest_present:, context:, wrap_single: true)
319
+ scope, soften = context
79
320
  if rhs_type.is_a?(Type::Tuple)
80
- decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present)
321
+ [*decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present, soften: soften), false]
322
+ elsif (element = array_element_type(rhs_type))
323
+ [*decompose_array(element, front_count, back_count, rest_present: rest_present), true]
324
+ elsif wrap_single && wraps_as_single_element?(rhs_type, scope)
325
+ wrapped = Type::Combinator.tuple_of(rhs_type)
326
+ [*decompose_tuple(wrapped, front_count, back_count, rest_present: rest_present, soften: soften), false]
81
327
  else
82
- decompose_default(front_count, back_count, rest_present: rest_present)
328
+ [*decompose_default(front_count, back_count, rest_present: rest_present), false]
83
329
  end
84
330
  end
85
331
 
86
- def decompose_tuple(tuple, front_count, back_count, rest_present:)
332
+ # Ruby's `[rhs]` wrap for a value with no implicit `to_ary`; see the module comment.
333
+ def wraps_as_single_element?(rhs_type, scope)
334
+ class_name = conversion_class_name(rhs_type)
335
+ !class_name.nil? && MethodDispatcher::RbsDispatch.array_conversion_free?(class_name, scope)
336
+ end
337
+
338
+ # Under an inherited mark the whole right-hand side may be the `nil` a short outer array padded in, and
339
+ # `(p, *q) = nil` binds `q = []`. A Tuple rest would claim the middle elements are present
340
+ # (`q.first.nil?` folding to `false`), so it widens to `Array[union of the middle]`, which admits the
341
+ # empty array; an empty middle has no element type to offer and stays `Array[Dynamic[top]]`.
342
+ def arity_free_rest(rest_type)
343
+ return rest_type unless rest_type.is_a?(Type::Tuple)
344
+
345
+ element = rest_type.elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*rest_type.elements)
346
+ Type::Combinator.nominal_of("Array", type_args: [element])
347
+ end
348
+
349
+ def decompose_array(element, front_count, back_count, rest_present:)
350
+ [
351
+ Array.new(front_count) { element },
352
+ rest_present ? Type::Combinator.nominal_of("Array", type_args: [element]) : nil,
353
+ Array.new(back_count) { element }
354
+ ]
355
+ end
356
+
357
+ def decompose_tuple(tuple, front_count, back_count, rest_present:, soften:)
87
358
  elements = tuple.elements
88
- fronts = Array.new(front_count) { |i| slot_type(elements, i) }
89
- if rest_present
90
- middle_end = [elements.size - back_count, front_count].max
91
- middle = elements[front_count...middle_end] || []
92
- rest_type = Type::Combinator.tuple_of(*middle)
93
- backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
94
- else
95
- rest_type = nil
96
- backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
97
- end
98
- [fronts, rest_type, backs]
359
+ fronts, backs = slot_offsets(elements.size, front_count, back_count, rest_present: rest_present)
360
+ .map { |offsets| offsets.map { |i| slot_type(elements, i, soften) } }
361
+ # The end is clamped at the fronts, as {slot_offsets} clamps the backs: an unclamped negative end
362
+ # (more back slots than elements) would count from the end instead. A range starting past the end
363
+ # slices to `nil`.
364
+ middle = elements[front_count...[elements.size - back_count, front_count].max] || []
365
+ [fronts, rest_present ? Type::Combinator.tuple_of(*middle) : nil, backs]
366
+ end
367
+
368
+ # The element offsets a `[fronts, backs]` pair of fixed slots reads out of `size` elements: the
369
+ # back slots follow the middle a rest absorbs, and follow the fronts directly without one. An
370
+ # offset past `size` is a slot the source is too short to fill.
371
+ def slot_offsets(size, front_count, back_count, rest_present:)
372
+ back_start = rest_present ? [size - back_count, front_count].max : front_count
373
+ [Array.new(front_count) { |i| i }, Array.new(back_count) { |i| back_start + i }]
99
374
  end
100
375
 
101
376
  # The per-slot type for index `i` of a tuple decomposition, FP-safely softened: a
@@ -116,11 +391,11 @@ module Rigor
116
391
  # outranks the worst-case per-slot reading, so we drop the `nil` from a destructured slot
117
392
  # and keep the non-`nil` constituent (a bare `nil` slot stays `nil` — there is nothing to
118
393
  # soften). A pure non-optional element keeps its precise type unchanged.
119
- def slot_type(elements, index)
394
+ def slot_type(elements, index, soften)
120
395
  element = elements[index]
121
396
  return Type::Combinator.constant_of(nil) if element.nil?
122
397
 
123
- soften_optional_slot(element)
398
+ soften ? soften_optional_slot(element) : element
124
399
  end
125
400
 
126
401
  def soften_optional_slot(element)
@@ -145,24 +420,42 @@ module Rigor
145
420
  ]
146
421
  end
147
422
 
148
- def bind_target(target, type, bindings)
149
- case target
150
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
151
- bindings[target.name] = type
152
- when Prism::MultiTargetNode
153
- visit(target, type, bindings)
154
- end
423
+ # A per-element Array mark reaches a name only through a nested target: a name bound to a
424
+ # whole literal array holds an Array, which is never `nil`. Nor does any mark reach a name bound
425
+ # to an exact `nil` — a slot past a Tuple's end or a wrap's empty slot — which is `nil` whether
426
+ # or not the source missed, so the mark could only withhold an honest verdict.
427
+ def bind_target(target, type, optimistic, bindings, marked, context)
428
+ return visit(target, type, optimistic, bindings, marked, context) if target.is_a?(Prism::MultiTargetNode)
429
+
430
+ key = binding_key(target)
431
+ bind_name(key, type, optimistic == true && !nil_literal?(type), bindings, marked) if key
155
432
  end
156
433
 
157
- def bind_rest_target(splat_node, type, bindings)
434
+ # The rest is an `Array` even when the right-hand side is short, so it is never marked.
435
+ def bind_rest_target(splat_node, type, bindings, marked)
158
436
  return unless splat_node.is_a?(Prism::SplatNode)
159
437
 
160
- expression = splat_node.expression
161
- case expression
162
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
163
- bindings[expression.name] = type
438
+ key = binding_key(splat_node.expression)
439
+ bind_name(key, type, false, bindings, marked) if key
440
+ end
441
+
442
+ # A named target's name, an index target's node (it binds no name, see {bind_name}), or nil for
443
+ # a target that contributes nothing to the scope.
444
+ def binding_key(target)
445
+ case target
446
+ when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::InstanceVariableTargetNode
447
+ target.name
448
+ when Prism::IndexTargetNode then target
164
449
  end
165
450
  end
451
+
452
+ # A later binding of the same name (`a, a = ints`) wins, mark included. `name` is the
453
+ # target node for an index target, which binds no name and so is never marked.
454
+ def bind_name(name, type, optimistic, bindings, marked)
455
+ bindings[name] = type
456
+ marked.delete(name)
457
+ marked << name if optimistic && name.is_a?(Symbol)
458
+ end
166
459
  end
167
460
  end
168
461
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "content_join"
5
+ require_relative "rewrite_mutation"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -48,7 +49,9 @@ module Rigor
48
49
  MutationWidening.join_added_pairs(pre_state, method_name, arg_types,
49
50
  [pinned_evidence(seed_keys), pinned_evidence(seed_values)])
50
51
  end
51
- rejoined = keep_precise_parameters(pre_state, rejoined)
52
+ # The arm lands after the precise parameters are restored: a rewriter falsifies a proven side too
53
+ # (`Hash.new(0)` under `transform_values!(&:to_s)` holds Strings), which an adder's one store cannot.
54
+ rejoined = RewriteMutation.arm(keep_precise_parameters(pre_state, rejoined), method_name)
52
55
  rejoined == nominal ? nil : rejoined
53
56
  end
54
57