rigortype 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -3,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,67 +73,299 @@ 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,
112
+ index_targets: NO_BINDINGS)
113
+ super
114
+ end
115
+
116
+ # Binds every name into `scope` and records the optimistic mark after the binding, since
117
+ # `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried before. `miss` is what
118
+ # each marked slot answers on a miss (issue #1302): `nil` for every mark the binder makes itself —
119
+ # a short array pads with `nil`, and a `nil` destructures to `nil` — so only a caller whose
120
+ # right-hand side a miss can make something else passes another answer
121
+ # ({OptimisticOrigin.destructuring_miss}).
122
+ def apply_to(scope, miss: nil)
123
+ bound = types.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
124
+ bound = ivars.reduce(bound) { |acc, (name, type)| acc.with_ivar(name, type) }
125
+ bound = optimistic.reduce(bound) do |acc, name|
126
+ acc.with_optimistic_local(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
127
+ end
128
+ optimistic_ivars.reduce(bound) do |acc, name|
129
+ acc.with_optimistic_ivar(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
130
+ end
131
+ end
132
+ end
133
+
51
134
  module_function
52
135
 
53
- # @param target_node [Prism::MultiWriteNode, Prism::MultiTargetNode]
54
- # @param rhs_type [Rigor::Type] type of the right-hand side
55
- # @return [Hash{Symbol => Rigor::Type}]
56
- def bind(target_node, rhs_type)
136
+ # @param rhs_type — type of the right-hand side
137
+ # @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
138
+ # question for a class outside the core list; without one only the core list wraps.
139
+ def bind(target_node, rhs_type, scope: nil)
140
+ bind_marked(target_node, rhs_type, scope: scope).types
141
+ end
142
+
143
+ # @param optimistic — whether `rhs_type` itself is an optimistic slot of an enclosing
144
+ # decomposition (a block's `|(g, h)|` fed from an auto-splatted `Array[T]`) or an
145
+ # optimistically nil-free value (`k, v = xs.first`), in which case every name bound under it
146
+ # inherits the mark. It may instead be the per-element Array
147
+ # {OptimisticOrigin.destructuring_marks} builds for a literal right-hand side
148
+ # (`x, y = xs.first, 1`), which marks the names under each fixed slot by that slot's element.
149
+ # @param soften_slots — false keeps a present `X | nil` tuple slot and a union member's bare
150
+ # `nil` in the binding instead of applying the ADR-57 softening of {slot_type} /
151
+ # {join_member_bindings}. That softening is only honest together with the optimistic mark
152
+ # it records, so a consumer that drops the marks (the class-ivar seed, issue #1110) turns it
153
+ # off; the `Array[T]` bet is unaffected, matching what `@x = xs.first` records.
154
+ def bind_marked(target_node, rhs_type, optimistic: false, scope: nil, soften_slots: true)
57
155
  bindings = {}
58
- visit(target_node, rhs_type, bindings)
59
- bindings
156
+ marked = []
157
+ visit(target_node, rhs_type, optimistic, bindings, marked, [scope, soften_slots])
158
+ split_result(bindings, marked)
159
+ end
160
+
161
+ # The per-slot types of a positional pattern (`in [i, s]`, `in [*pre, m, *post]`), sharing
162
+ # every carrier rule {.bind_marked} applies to a multi-write target except two statement-only
163
+ # properties (issue #1122):
164
+ #
165
+ # - the multi-assign `[rhs]` wrap is NOT applied. `a, b = 1` binds `a` to `1` because Ruby wraps
166
+ # a right-hand side with no implicit `to_ary`; an array pattern instead matches through the
167
+ # subject's `deconstruct` / `to_ary`, and a subject with neither raises
168
+ # `NoMatchingPatternError` — no body is reached, so binding `1` there would type dead code.
169
+ # - no optimistic mark is reported. The ADR-101 short-array bet exists because a SHORT
170
+ # right-hand side pads the fixed slots with `nil`; a pattern that matched has every fixed slot
171
+ # it named, so there is nothing to bet on and `Result#optimistic` has no counterpart here.
172
+ #
173
+ # A `Type::Union` is the caller's to distribute: a pattern matches SOME member, and only the
174
+ # caller can tell which members can match it at all. Returns `[fronts, rest_type, backs]`, with
175
+ # every carrier no rule decomposes answering `Dynamic[Top]` per slot — the floor.
176
+ def decompose_slots(rhs_type, front_count:, back_count:, rest_present:, scope: nil, soften_slots: true)
177
+ fronts, rest_type, backs, = decompose(
178
+ rhs_type, front_count, back_count, rest_present: rest_present,
179
+ context: [scope, soften_slots], wrap_single: false
180
+ )
181
+ [fronts, rest_type, backs]
182
+ end
183
+
184
+ # The `T` of an `Array[T]` carrier the binder may decompose, or nil. Declines raw `Array`
185
+ # and an untyped / top element, which bind `Dynamic[Top]` per slot as before, and every
186
+ # `Dynamic` wrapper: `Dynamic[Array[T]]` is gradual, and projecting its static facet would
187
+ # hand the slot a bare `T` that licenses the negative rules (ADR-5). Shared with
188
+ # {BlockParameterBinder}'s auto-splat so both surfaces accept the same carriers.
189
+ def array_element_type(type)
190
+ case type
191
+ when Type::Nominal
192
+ return nil unless type.class_name == "Array" && type.type_args.size == 1
193
+
194
+ element = type.type_args.first
195
+ element.is_a?(Type::Dynamic) || element.is_a?(Type::Top) ? nil : element
196
+ when Type::Refined, Type::Difference
197
+ array_element_type(type.base)
198
+ end
199
+ end
200
+
201
+ # The class whose instances `type` describes, for the `to_ary` question, or nil when `type`
202
+ # names no single class: `Dynamic`, `Top`, `Singleton`, `Intersection` and the rest decline.
203
+ # A union never reaches here; the binder distributes it first.
204
+ def conversion_class_name(type)
205
+ case type
206
+ when Type::Constant then type.value.class.name
207
+ when Type::Nominal then type.class_name
208
+ when Type::HashShape then "Hash"
209
+ when Type::IntegerRange then "Integer"
210
+ when Type::Refined, Type::Difference then conversion_class_name(type.base)
211
+ end
60
212
  end
61
213
 
62
214
  class << self
63
215
  private
64
216
 
65
- def visit(node, rhs_type, bindings)
217
+ # Partitions the walk's single map: an ivar name carries its `@` sigil, which a local name
218
+ # never does, and an index target is keyed by its node rather than by a name.
219
+ def split_result(bindings, marked)
220
+ return Result.new(types: bindings, optimistic: marked.freeze) if bindings.each_key.all? { |k| local_key?(k) }
221
+
222
+ grouped = bindings.group_by { |key, _| key_kind(key) }.transform_values(&:to_h)
223
+ ivar_marked, local_marked = marked.partition { |name| key_kind(name) == :ivar }
224
+ Result.new(types: grouped.fetch(:local, {}), optimistic: local_marked.freeze,
225
+ ivars: grouped.fetch(:ivar, NO_BINDINGS), optimistic_ivars: ivar_marked.freeze,
226
+ index_targets: grouped.fetch(:index, NO_BINDINGS))
227
+ end
228
+
229
+ def local_key?(key) = key.is_a?(Symbol) && !key.start_with?("@")
230
+
231
+ def key_kind(key)
232
+ return :index unless key.is_a?(Symbol)
233
+
234
+ key.start_with?("@") ? :ivar : :local
235
+ end
236
+
237
+ # `context` is the `[scope, soften_slots]` pair every step of the walk shares.
238
+ def visit(node, rhs_type, optimistic, bindings, marked, context)
239
+ if rhs_type.is_a?(Type::Union)
240
+ return visit_union(node, rhs_type.members, optimistic, bindings, marked, context)
241
+ end
242
+
66
243
  lefts = node.lefts || []
67
244
  rest = node.rest
68
245
  rights = node.rights || []
246
+ rest_present = !rest.nil?
69
247
 
70
- fronts, rest_type, backs = decompose(rhs_type, lefts.size, rights.size, rest_present: !rest.nil?)
71
- lefts.each_with_index { |t, i| bind_target(t, fronts[i], bindings) }
72
- bind_rest_target(rest, rest_type, bindings) if rest
73
- rights.each_with_index { |t, i| bind_target(t, backs[i], bindings) }
248
+ fronts, rest_type, backs, slots_optimistic =
249
+ decompose(rhs_type, lefts.size, rights.size, rest_present: rest_present, context: context)
250
+ rest_type = arity_free_rest(rest_type) if optimistic == true
251
+ front_marks, back_marks =
252
+ slot_marks(optimistic, slots_optimistic, rhs_type, [lefts.size, rights.size], rest_present: rest_present)
253
+ lefts.each_with_index { |t, i| bind_target(t, fronts[i], front_marks[i], bindings, marked, context) }
254
+ bind_rest_target(rest, rest_type, bindings, marked) if rest
255
+ rights.each_with_index { |t, i| bind_target(t, backs[i], back_marks[i], bindings, marked, context) }
256
+ end
257
+
258
+ # The `[front_marks, back_marks]` pair a visit hands its fixed slots. A boolean `optimistic`
259
+ # applies to every slot, as does the decomposition's own short-array bet (`fallback`, the
260
+ # issue #1093 `Array[T]` mark). A per-element Array applies only to the `Type::Tuple` a literal
261
+ # right-hand side types as, of the same arity, and each slot picks its element at the offset
262
+ # {decompose_tuple} reads ({slot_offsets}); any other carrier falls back to `fallback`, and a
263
+ # slot past the literal's end binds an exact `nil` that no mark qualifies.
264
+ def slot_marks(optimistic, fallback, rhs_type, (front_count, back_count), rest_present:)
265
+ literal = optimistic.is_a?(Array) && rhs_type.is_a?(Type::Tuple) && rhs_type.elements.size == optimistic.size
266
+ uniform = optimistic.is_a?(Array) ? fallback : optimistic || fallback
267
+ return [[uniform] * front_count, [uniform] * back_count] unless literal
268
+
269
+ slot_offsets(optimistic.size, front_count, back_count, rest_present: rest_present)
270
+ .map { |offsets| offsets.map { |i| optimistic.fetch(i, false) } }
271
+ end
272
+
273
+ # Every member walks the same target tree, so each binds the same names; the first member's
274
+ # key order is the declaration order.
275
+ def visit_union(node, members, optimistic, bindings, marked, context)
276
+ walks = members.map do |member|
277
+ member_bindings = {}
278
+ member_marked = []
279
+ visit(node, member, optimistic, member_bindings, member_marked, context)
280
+ [member_bindings, member_marked]
281
+ end
282
+ walks.first.first.each_key do |name|
283
+ types = walks.map { |member_bindings, _| member_bindings[name] }
284
+ joined, softened = join_member_bindings(types, context.last)
285
+ mark = softened || walks.any? { |_, member_marked| member_marked.include?(name) }
286
+ bind_name(name, joined, mark, bindings, marked)
287
+ end
288
+ end
289
+
290
+ # Returns `[type, softened]`. `Dynamic[Top]` from any member is the whole answer: the join
291
+ # must not let a decomposable member's precise type stand for a member nothing is known
292
+ # about. A member that binds the name to bare `nil` — a `nil` slot, a slot past a short
293
+ # member, or a `nil` member wrapped as `[nil]` — is left out of the join when another member
294
+ # binds a value, and `softened` reports it so the caller marks the name optimistic. This is
295
+ # {slot_type}'s ADR-57 softening across members: which member arrived is correlated with the
296
+ # other slots (`k, v = hash.find { ... }; v.x if k`, `status, value = ok ? [:ok, v] : [:err]`),
297
+ # and a per-slot `T | nil` fires `call.possible-nil-receiver` on the guarded read.
298
+ def join_member_bindings(types, soften_slots)
299
+ untyped = Type::Combinator.untyped
300
+ return [untyped, false] if types.any? { |type| type == untyped }
301
+
302
+ firm = types.reject { |type| nil_literal?(type) }
303
+ return [Type::Combinator.union(*types), false] if !soften_slots || firm.empty? || firm.size == types.size
304
+
305
+ [Type::Combinator.union(*firm), true]
74
306
  end
75
307
 
76
308
  # Decomposes the right-hand side type into the per-slot types. Returns a `[fronts,
77
- # rest_type, backs]` triple, with `fronts` and `backs` each an ordered array of length
78
- # `front_count`/`back_count`, and `rest_type` either a `Rigor::Type` (when
79
- # `rest_present:` is true) or `nil`.
80
- def decompose(rhs_type, front_count, back_count, rest_present:)
309
+ # rest_type, backs, optimistic]` quadruple, with `fronts` and `backs` each an ordered
310
+ # array of length `front_count`/`back_count`, `rest_type` either a `Rigor::Type` (when
311
+ # `rest_present:` is true) or `nil`, and `optimistic` true when the fixed slots are the
312
+ # short-array bet rather than known elements.
313
+ def decompose(rhs_type, front_count, back_count, rest_present:, context:, wrap_single: true)
314
+ scope, soften = context
81
315
  if rhs_type.is_a?(Type::Tuple)
82
- decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present)
316
+ [*decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present, soften: soften), false]
317
+ elsif (element = array_element_type(rhs_type))
318
+ [*decompose_array(element, front_count, back_count, rest_present: rest_present), true]
319
+ elsif wrap_single && wraps_as_single_element?(rhs_type, scope)
320
+ wrapped = Type::Combinator.tuple_of(rhs_type)
321
+ [*decompose_tuple(wrapped, front_count, back_count, rest_present: rest_present, soften: soften), false]
83
322
  else
84
- decompose_default(front_count, back_count, rest_present: rest_present)
323
+ [*decompose_default(front_count, back_count, rest_present: rest_present), false]
85
324
  end
86
325
  end
87
326
 
88
- def decompose_tuple(tuple, front_count, back_count, rest_present:)
327
+ # Ruby's `[rhs]` wrap for a value with no implicit `to_ary`; see the module comment.
328
+ def wraps_as_single_element?(rhs_type, scope)
329
+ class_name = conversion_class_name(rhs_type)
330
+ !class_name.nil? && MethodDispatcher::RbsDispatch.array_conversion_free?(class_name, scope)
331
+ end
332
+
333
+ # Under an inherited mark the whole right-hand side may be the `nil` a short outer array padded in, and
334
+ # `(p, *q) = nil` binds `q = []`. A Tuple rest would claim the middle elements are present
335
+ # (`q.first.nil?` folding to `false`), so it widens to `Array[union of the middle]`, which admits the
336
+ # empty array; an empty middle has no element type to offer and stays `Array[Dynamic[top]]`.
337
+ def arity_free_rest(rest_type)
338
+ return rest_type unless rest_type.is_a?(Type::Tuple)
339
+
340
+ element = rest_type.elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*rest_type.elements)
341
+ Type::Combinator.nominal_of("Array", type_args: [element])
342
+ end
343
+
344
+ def decompose_array(element, front_count, back_count, rest_present:)
345
+ [
346
+ Array.new(front_count) { element },
347
+ rest_present ? Type::Combinator.nominal_of("Array", type_args: [element]) : nil,
348
+ Array.new(back_count) { element }
349
+ ]
350
+ end
351
+
352
+ def decompose_tuple(tuple, front_count, back_count, rest_present:, soften:)
89
353
  elements = tuple.elements
90
- fronts = Array.new(front_count) { |i| slot_type(elements, i) }
91
- if rest_present
92
- middle_end = [elements.size - back_count, front_count].max
93
- middle = elements[front_count...middle_end] || []
94
- rest_type = Type::Combinator.tuple_of(*middle)
95
- backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
96
- else
97
- rest_type = nil
98
- backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
99
- end
100
- [fronts, rest_type, backs]
354
+ fronts, backs = slot_offsets(elements.size, front_count, back_count, rest_present: rest_present)
355
+ .map { |offsets| offsets.map { |i| slot_type(elements, i, soften) } }
356
+ # The end is clamped at the fronts, as {slot_offsets} clamps the backs: an unclamped negative end
357
+ # (more back slots than elements) would count from the end instead. A range starting past the end
358
+ # slices to `nil`.
359
+ middle = elements[front_count...[elements.size - back_count, front_count].max] || []
360
+ [fronts, rest_present ? Type::Combinator.tuple_of(*middle) : nil, backs]
361
+ end
362
+
363
+ # The element offsets a `[fronts, backs]` pair of fixed slots reads out of `size` elements: the
364
+ # back slots follow the middle a rest absorbs, and follow the fronts directly without one. An
365
+ # offset past `size` is a slot the source is too short to fill.
366
+ def slot_offsets(size, front_count, back_count, rest_present:)
367
+ back_start = rest_present ? [size - back_count, front_count].max : front_count
368
+ [Array.new(front_count) { |i| i }, Array.new(back_count) { |i| back_start + i }]
101
369
  end
102
370
 
103
371
  # The per-slot type for index `i` of a tuple decomposition, FP-safely softened: a
@@ -118,11 +386,11 @@ module Rigor
118
386
  # outranks the worst-case per-slot reading, so we drop the `nil` from a destructured slot
119
387
  # and keep the non-`nil` constituent (a bare `nil` slot stays `nil` — there is nothing to
120
388
  # soften). A pure non-optional element keeps its precise type unchanged.
121
- def slot_type(elements, index)
389
+ def slot_type(elements, index, soften)
122
390
  element = elements[index]
123
391
  return Type::Combinator.constant_of(nil) if element.nil?
124
392
 
125
- soften_optional_slot(element)
393
+ soften ? soften_optional_slot(element) : element
126
394
  end
127
395
 
128
396
  def soften_optional_slot(element)
@@ -147,24 +415,42 @@ module Rigor
147
415
  ]
148
416
  end
149
417
 
150
- def bind_target(target, type, bindings)
151
- case target
152
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
153
- bindings[target.name] = type
154
- when Prism::MultiTargetNode
155
- visit(target, type, bindings)
156
- end
418
+ # A per-element Array mark reaches a name only through a nested target: a name bound to a
419
+ # whole literal array holds an Array, which is never `nil`. Nor does any mark reach a name bound
420
+ # to an exact `nil` — a slot past a Tuple's end or a wrap's empty slot — which is `nil` whether
421
+ # or not the source missed, so the mark could only withhold an honest verdict.
422
+ def bind_target(target, type, optimistic, bindings, marked, context)
423
+ return visit(target, type, optimistic, bindings, marked, context) if target.is_a?(Prism::MultiTargetNode)
424
+
425
+ key = binding_key(target)
426
+ bind_name(key, type, optimistic == true && !nil_literal?(type), bindings, marked) if key
157
427
  end
158
428
 
159
- def bind_rest_target(splat_node, type, bindings)
429
+ # The rest is an `Array` even when the right-hand side is short, so it is never marked.
430
+ def bind_rest_target(splat_node, type, bindings, marked)
160
431
  return unless splat_node.is_a?(Prism::SplatNode)
161
432
 
162
- expression = splat_node.expression
163
- case expression
164
- when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
165
- bindings[expression.name] = type
433
+ key = binding_key(splat_node.expression)
434
+ bind_name(key, type, false, bindings, marked) if key
435
+ end
436
+
437
+ # A named target's name, an index target's node (it binds no name, see {bind_name}), or nil for
438
+ # a target that contributes nothing to the scope.
439
+ def binding_key(target)
440
+ case target
441
+ when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::InstanceVariableTargetNode
442
+ target.name
443
+ when Prism::IndexTargetNode then target
166
444
  end
167
445
  end
446
+
447
+ # A later binding of the same name (`a, a = ints`) wins, mark included. `name` is the
448
+ # target node for an index target, which binds no name and so is never marked.
449
+ def bind_name(name, type, optimistic, bindings, marked)
450
+ bindings[name] = type
451
+ marked.delete(name)
452
+ marked << name if optimistic && name.is_a?(Symbol)
453
+ end
168
454
  end
169
455
  end
170
456
  end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "content_join"
5
+ require_relative "rewrite_mutation"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # The re-join half of the mutation widening (issue #580): what happens to the SECOND and later
10
+ # content mutations of one collection, once {MutationWidening} has already replaced the literal
11
+ # carrier with a `Nominal`.
12
+ #
13
+ # Split out of {MutationWidening} because it answers a different question. That module decides
14
+ # what a mutation FALSIFIES about a literal shape; this one decides when a carrier is still open
15
+ # enough to LEARN from, which is a provenance question the type system has no field for and this
16
+ # file answers off the carrier itself.
17
+ module MutationRejoin
18
+ module_function
19
+
20
+ # Re-opens a carrier this seam already widened once, so the SECOND and later stores into the
21
+ # same collection are seen (issue #580).
22
+ #
23
+ # Before this arm the widening was a one-way door: the first content mutation replaced the
24
+ # literal `Tuple` / `HashShape` with a `Nominal`, `widen_for_mutator` had no `Nominal` arm, and
25
+ # `a = []; a.push(1); a.push("s")` closed at `Array[Integer | Dynamic[top]]` with the `String`
26
+ # arm simply absent. The `Dynamic` arm kept that from being a WRONG answer — nothing folds and
27
+ # nothing dispatches off a union carrying it — but the carrier still did not say what the
28
+ # program holds, and every precision lever that would eventually take the gradual arm off had
29
+ # to inherit the missing store.
30
+ #
31
+ # `nil` when the re-join changed nothing, so a non-adder (`pop`, `sort!`) on an already-widened
32
+ # carrier does not rebind the binding at all: a rebind drops the per-slot narrowings `with_local`
33
+ # keys to the old value, and paying that for an identical type is a straight loss.
34
+ def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES)
35
+ return nil unless regrowable_carrier?(nominal)
36
+
37
+ pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
38
+ rejoined =
39
+ case nominal.class_name
40
+ when "Array"
41
+ return nil unless MutationWidening::ARRAY_MUTATORS.include?(method_name)
42
+
43
+ MutationWidening.join_added_elements(pre_state, method_name, arg_types,
44
+ pinned_evidence(ContentJoin.collection_element_types(pre_state)))
45
+ when "Hash"
46
+ return nil unless MutationWidening::HASH_MUTATORS.include?(method_name)
47
+
48
+ seed_keys, seed_values = ContentJoin.hash_shape_key_values(pre_state)
49
+ MutationWidening.join_added_pairs(pre_state, method_name, arg_types,
50
+ [pinned_evidence(seed_keys), pinned_evidence(seed_values)])
51
+ end
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)
55
+ rejoined == nominal ? nil : rejoined
56
+ end
57
+
58
+ # Re-grows only the parameters that were already gradual, restoring every other one.
59
+ #
60
+ # {#regrowable_carrier?} asks the carrier as a whole, but a `Hash` has two parameters and they
61
+ # can disagree: `Hash.new(0)` is `Hash[Dynamic[top], Integer]` — an open key side over a value
62
+ # side the default-arg fold PROVED. Re-joining both put the gradual floor on the value side and
63
+ # cost the counter idiom (`h = Hash.new(0); h[:x] += 1; h[:x]`) its `Integer` for a
64
+ # `Dynamic[top] | Integer`, a precision loss for evidence the key side alone was missing.
65
+ #
66
+ # The restored side comes from the pin-widened pre-state rather than the original carrier, so a
67
+ # slot-rewriting mutator still falsifies the pinning it falsifies.
68
+ def keep_precise_parameters(pre_state, rejoined)
69
+ return rejoined unless rejoined.is_a?(Type::Nominal) && rejoined.type_args.size == pre_state.type_args.size
70
+
71
+ args = pre_state.type_args.each_with_index.map do |arg, i|
72
+ gradual_parameter?(arg) ? rejoined.type_args[i] : arg
73
+ end
74
+ Type::Combinator.nominal_of(rejoined.class_name, type_args: args)
75
+ end
76
+
77
+ # The class claim a re-opened carrier still makes, for {ContentJoin.admissible_evidence} to gate
78
+ # the added members against.
79
+ #
80
+ # It is the carrier's VALUE-PINNED arms and nothing else, because pinning is the only trace a
81
+ # literal seed's own elements leave once the carrier is nominal. Reading the whole element list
82
+ # instead gets both ends wrong. {ContentJoin.admissible_evidence} answers "everything is
83
+ # admissible" for a seed carrying a `Dynamic`, and a re-opened carrier ALWAYS carries one — its
84
+ # own gradual floor — so the gate would be off exactly where it is needed: haml's
85
+ # `temple = [:multi]` grew an `Array[…]` arm past its hand-written `-> Array[:multi]` and brought
86
+ # the #561 `def.return-type-mismatch` back. Gating on the full list is the opposite error — after
87
+ # `a = []; a.push(1)` the carrier reads `Array[Integer | Dynamic[top]]`, and a class set of
88
+ # `{Integer}` floors the `"s"` the next line stores, which is the whole answer this issue is
89
+ # about. The pinning tells the two apart: `:multi` is a claim the author's literal made, `Integer`
90
+ # is this seam's own join talking back to itself.
91
+ #
92
+ # No pinned arm means no claim to contradict — an empty-seeded accumulator — and the added
93
+ # evidence is admitted as itself.
94
+ def pinned_evidence(members)
95
+ members.flat_map { |m| ContentJoin.union_members(m) }.grep(Type::Constant)
96
+ end
97
+
98
+ # Which nominals may re-grow — the FP boundary this whole issue turns on, read off the carrier
99
+ # itself rather than out of a side table.
100
+ #
101
+ # A carrier that already carries a gradual arm on the parameter the mutation feeds is one this
102
+ # seam (or a declaration) has already declared open. Growing it is free of the #561 blast
103
+ # radius by construction: adding a member to a union that already contains `Dynamic[top]`
104
+ # cannot make the union reject a declared type it accepted, cannot manufacture a constant fold
105
+ # (a union carrying `Dynamic` never folds), and cannot make a dispatch that was quiet fire.
106
+ # What it CAN do is stop dropping evidence — which is the whole point.
107
+ #
108
+ # A PRECISE nominal is refused, and that is exactly the case the issue names: haml's
109
+ # hand-written `-> Array[:multi]`, or an `Array[Symbol]` an RBS signature declares. Its element
110
+ # set is a claim somebody made about the collection, not a residue of this seam's own
111
+ # one-store guesswork, and joining a foreign member into it is what drew eight
112
+ # `def.return-type-mismatch` on correct code (PR #561). The seed-admissibility gate in
113
+ # {ContentJoin.admissible_evidence} stays the guard for everything that does get through.
114
+ #
115
+ # This is a superset of "mutation-widened": a DECLARED `Array[untyped]` or
116
+ # `Array[Integer | untyped]` matches too. Deliberate — the soundness argument above is about
117
+ # the carrier being open, not about who opened it, and a declared gradual arm is the author
118
+ # saying the collection may hold anything. Distinguishing the two needs provenance travelling
119
+ # WITH the value (a field on `Type::Nominal`, reopening structural equality, `Marshal`, and the
120
+ # cache schema); nothing on this axis needs it yet.
121
+ def regrowable_carrier?(nominal)
122
+ return false unless %w[Array Hash].include?(nominal.class_name)
123
+
124
+ nominal.type_args.any? { |arg| gradual_parameter?(arg) }
125
+ end
126
+
127
+ def gradual_parameter?(type)
128
+ ContentJoin.union_members(type).any?(Type::Dynamic)
129
+ end
130
+
131
+ # The value-rewriting counterpart of {MutationWidening.widen_tuple}'s `values: :widen`. A mutator that lands a
132
+ # new value in an EXISTING slot falsifies that slot's pinning on a re-opened carrier exactly as
133
+ # it does on a literal one, so `d = [:multi]; d << x; d[0] = "s"` must not keep reading `:multi`
134
+ # back out of slot 0.
135
+ def value_pin_widened_args(nominal)
136
+ Type::Combinator.nominal_of(nominal.class_name,
137
+ type_args: nominal.type_args.map { |a| Type::Combinator.widen_value_pinned(a) })
138
+ end
139
+
140
+ # Rebinds `name` to the widened carrier, carrying across what a re-join must not lose.
141
+ def rebind(scope, builder, name, widened, pre_state:, kind:)
142
+ rebound = scope.public_send(builder, name, widened)
143
+ pre_state.is_a?(Type::Nominal) ? carry_slot_narrowings(rebound, scope, kind, name) : rebound
144
+ end
145
+
146
+ # Re-installs the per-slot `receiver[key] ||= default` narrowings that rebinding the receiver
147
+ # drops.
148
+ #
149
+ # `Scope#with_local` drops them because a rebind normally means a DIFFERENT object, and the
150
+ # slot's non-nil guarantee was about the old one. A re-join (issue #580) is the opposite: the
151
+ # binding still names the same object, and only the summary of what it holds grew. Before the
152
+ # `Nominal` arm existed the second store into an already-widened carrier rebound nothing at
153
+ # all, so the narrowing survived by accident — Redmine's `params[:f] ||= []` then
154
+ # `params[:op] ||= {}` idiom lost `:f`'s narrowing the moment `:op` re-joined, and the next
155
+ # `params[:f] << :status` dispatched on nil again. Carrying them keeps the re-join's scope
156
+ # effect to the one thing it actually learned.
157
+ #
158
+ # The call recording its OWN key runs after the widening (`eval_index_or_write` orders it that
159
+ # way deliberately), so a carried entry never shadows a fresher one for the same slot.
160
+ # Method-chain narrowings are deliberately NOT carried: `x.last` is a claim about a position a
161
+ # content mutation really can move.
162
+ def carry_slot_narrowings(rebound, pre_scope, kind, name)
163
+ pre_scope.indexed_narrowings.reduce(rebound) do |acc, (key, type)|
164
+ next acc unless key.receiver_kind == kind && key.receiver_name == name.to_sym
165
+
166
+ acc.with_indexed_narrowing(key.receiver_kind, key.receiver_name, key.key, type)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end