rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -31,7 +31,12 @@ module Rigor
31
31
  # `nil` when the re-join changed nothing, so a non-adder (`pop`, `sort!`) on an already-widened
32
32
  # carrier does not rebind the binding at all: a rebind drops the per-slot narrowings `with_local`
33
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)
34
+ def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES,
35
+ synthesised: false)
36
+ if synthesised && (synthesised_carrier?(nominal) || partially_gradual_hash?(nominal))
37
+ return widen_synthesised(nominal, method_name, values:, arg_types:)
38
+ end
39
+ return widen_precise_array(nominal, method_name, values:, arg_types:) if precise_array?(nominal)
35
40
  return nil unless regrowable_carrier?(nominal)
36
41
 
37
42
  pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
@@ -115,19 +120,250 @@ module Rigor
115
120
  # This is a superset of "mutation-widened": a DECLARED `Array[untyped]` or
116
121
  # `Array[Integer | untyped]` matches too. Deliberate — the soundness argument above is about
117
122
  # 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.
123
+ # saying the collection may hold anything.
124
+ #
125
+ # Issue #2555 L3 — the provenance this boundary turns on travels beside the binding, not with the value: a
126
+ # local whose object the engine built carries the `:synthesised` construction fact ({Scope#construction_fact?}),
127
+ # and {#widen_synthesised} answers for it before this predicate is asked. A field on `Type::Nominal` was the
128
+ # other route; `Combinator.union`'s structural dedupe would keep whichever tagged twin it met first.
121
129
  def regrowable_carrier?(nominal)
122
130
  return false unless %w[Array Hash].include?(nominal.class_name)
123
131
 
124
132
  nominal.type_args.any? { |arg| gradual_parameter?(arg) }
125
133
  end
126
134
 
135
+ # Issue #2555 L3 — a PRECISE `Array` / `Hash` nominal (no parameter carries a `Dynamic` arm), the carrier
136
+ # {#widen_synthesised} re-grows when its binding carries the `:synthesised` fact. A carrier that already carries
137
+ # a gradual arm keeps the ordinary re-join, whose floor it holds already.
138
+ def synthesised_carrier?(nominal)
139
+ %w[Array Hash].include?(nominal.class_name) && !nominal.type_args.empty? &&
140
+ nominal.type_args.none? { |arg| gradual_parameter?(arg) }
141
+ end
142
+
143
+ # Issue #2555 L3 — a `Hash` with one gradual parameter and one precise one: `Hash.new(0)` is
144
+ # `Hash[Dynamic[top], Integer]`. On a synthesised binding the precise side is this engine's own fold of the
145
+ # default, not a declaration, so {#keep_precise_parameters} must not restore it over a store:
146
+ # `h = Hash.new(0); h[:y] = "s"` reads `Hash[Dynamic[top] | Symbol, Integer | String]`, not
147
+ # `Hash[Dynamic[top] | Symbol, Integer]` (which made `h[:y].upcase` an `undefined method` on Integer). The
148
+ # counter idiom keeps its `Integer`: `h[:x] += 1` stores an `Integer`.
149
+ def partially_gradual_hash?(nominal)
150
+ nominal.class_name == "Hash" && nominal.type_args.size == 2 &&
151
+ nominal.type_args.any? { |arg| gradual_parameter?(arg) } &&
152
+ nominal.type_args.any? { |arg| !gradual_parameter?(arg) }
153
+ end
154
+
155
+ # Issue #2555 L3 — the re-join for a precise carrier the engine built itself: a collection literal closed by a
156
+ # value-preserving mutator (`a = [1, 2]; a.sort!` is `Array[1 | 2]`, `g = { a: 1 }; g.delete(:b)` is
157
+ # `Hash[Symbol, 1]`), a constructor fold (`Array.new(n, 0)` is `Array[Integer]`), or a literal built as a
158
+ # nominal outright (`{ ENV["A"].to_s => 1 }`). Its element types are this engine's inference, not a claim a
159
+ # declaration made, so the #561 boundary ({#regrowable_carrier?}, {#precise_admitted}) does not apply:
160
+ #
161
+ # - an added member joins as itself (value pin and literal shape dropped, {MutationWidening.value_pin_widened}),
162
+ # with no seed-admissibility gate and no `Dynamic[top]` floor — `Array.new(n, 0) << "s"` reads
163
+ # `Array[Integer | String]`;
164
+ # - a rewriter takes {RewriteMutation.arm}'s gradual position, as a gradual carrier does —
165
+ # `g.transform_values!(&:to_s)` reads `Hash[Symbol, Dynamic[top]]`;
166
+ # - an adder whose arguments yield no evidence is one unknown store and joins `Dynamic[top]`; one called with no
167
+ # argument types at all (the block-capture seam, whose content join adds the stores afterwards) changes
168
+ # nothing.
169
+ #
170
+ # The floor the ordinary re-join adds exists because a carrier it closed could not re-grow; this one stays
171
+ # re-growable while the binding keeps the fact, so every later store made THROUGH THE BINDING is seen. A store
172
+ # through another holder is not, which is why a local that may escape anywhere in its body never carries the
173
+ # fact ({ConstructionEscape}). Nor is a remover taking a store out again, which is why a remover on a carrier a
174
+ # store has grown restores the floor ({MutationWidening.floor_grown_carrier}).
175
+ def widen_synthesised(nominal, method_name, values:, arg_types:)
176
+ pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
177
+ rejoined =
178
+ case nominal.class_name
179
+ when "Array"
180
+ return nil unless MutationWidening::ARRAY_MUTATORS.include?(method_name)
181
+
182
+ synthesised_array_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
183
+ when "Hash"
184
+ return nil unless MutationWidening::HASH_MUTATORS.include?(method_name)
185
+
186
+ synthesised_hash_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
187
+ end
188
+ rejoined == nominal ? nil : rejoined
189
+ end
190
+
191
+ # Issue #2555 — the removers and filters of an Array, a Hash or a Set: each can take a stored member out again,
192
+ # which nothing narrows the carrier back from.
193
+ SYNTHESISED_REMOVERS = %i[
194
+ pop shift slice! delete delete? delete_at delete_if reject! select! filter! keep_if compact! clear subtract
195
+ ].to_set.freeze
196
+
197
+ # The adders whose store marks a synthesised binding `:grown`.
198
+ def content_adder?(method_name)
199
+ ContentJoin::CONTENT_ADDERS.include?(method_name) || SetMutation::CONTENT_ADDERS.include?(method_name)
200
+ end
201
+
202
+ # Whether a store turned `current` into `widened` by changing an Array / Hash / Set parameter that carried no
203
+ # `Dynamic` arm before it: a member joined where no floor covered it, which a later remover could leave stale.
204
+ # A store the parameter already held, or one that joined beside an earlier floor (`Hash.new(0)`'s key side),
205
+ # grows nothing.
206
+ def grew_unfloored?(current, widened)
207
+ return false if widened.nil?
208
+
209
+ before = ContentJoin.union_members(current)
210
+ ContentJoin.union_members(widened).any? do |member|
211
+ next false unless member.is_a?(Type::Nominal) && %w[Array Hash Set].include?(member.class_name)
212
+
213
+ prior = before.find { |b| same_carrier?(b, member) }
214
+ member.type_args.each_with_index.any? do |arg, i|
215
+ prior.nil? || (!gradual_parameter?(prior.type_args[i]) && prior.type_args[i] != arg)
216
+ end
217
+ end
218
+ end
219
+
220
+ def same_carrier?(type, nominal)
221
+ type.is_a?(Type::Nominal) && type.class_name == nominal.class_name &&
222
+ type.type_args.size == nominal.type_args.size
223
+ end
224
+
225
+ # Whether `method_name` on the synthesised local `name` takes the floor: a remover on a carrier a store grew.
226
+ def floors_grown_carrier?(scope, name, method_name)
227
+ SYNTHESISED_REMOVERS.include?(method_name) && scope.construction_fact?(name, :grown)
228
+ end
229
+
230
+ # `type` with every Array / Hash / Set parameter joined with `Dynamic[top]`, member-wise through a union: the
231
+ # reading the carrier would have had without the fact. With `drop_nil` (a remover that can take a `nil` out,
232
+ # {MutationWidening.removes_nil?}) the parameter's `nil` member goes too: on the path without the fact a stored
233
+ # `nil` the seed did not accept joined as the floor alone ({#precise_admitted}), and a `nil` kept beside the floor
234
+ # would still draw `call.possible-nil-receiver` after `parts << nil; parts.compact!`. Any other remover keeps the
235
+ # `nil`, which it may leave behind (`s << nil; s.delete(1)`).
236
+ def floor_parameters(type, drop_nil: false)
237
+ case type
238
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| floor_parameters(member, drop_nil:) })
239
+ when Type::Nominal
240
+ return type unless %w[Array Hash Set].include?(type.class_name) && !type.type_args.empty?
241
+
242
+ Type::Combinator.nominal_of(type.class_name,
243
+ type_args: type.type_args.map { |arg| floored_parameter(arg, drop_nil) })
244
+ else type
245
+ end
246
+ end
247
+
248
+ def hash_carrier?(type)
249
+ ContentJoin.union_members(type).any? { |member| member.is_a?(Type::Nominal) && member.class_name == "Hash" }
250
+ end
251
+
252
+ def floored_parameter(arg, drop_nil)
253
+ kept = ContentJoin.union_members(arg)
254
+ kept = kept.reject { |member| member.is_a?(Type::Constant) && member.value.nil? } if drop_nil
255
+ Type::Combinator.union(*kept, Type::Combinator.untyped)
256
+ end
257
+
258
+ def synthesised_array_join(carrier, method_name, arg_types)
259
+ return carrier unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
260
+
261
+ added = MutationWidening.value_pin_widened(ContentJoin.array_added_elements(method_name, arg_types))
262
+ ContentJoin.join_array_content(carrier, added.empty? ? [Type::Combinator.untyped] : added)
263
+ end
264
+
265
+ def synthesised_hash_join(carrier, method_name, arg_types)
266
+ return carrier unless ContentJoin::HASH_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
267
+
268
+ # A `nil` key marks a splat, an unknown key ({MutationWidening.join_added_pairs}).
269
+ key, value = MutationWidening.value_pin_widened([arg_types.first || Type::Combinator.untyped, arg_types.last])
270
+ value ||= Type::Combinator.untyped
271
+ ContentJoin.join_hash_content(carrier, [[key, value]])
272
+ end
273
+
127
274
  def gradual_parameter?(type)
128
275
  ContentJoin.union_members(type).any?(Type::Dynamic)
129
276
  end
130
277
 
278
+ # A typed `Array[E]` whose `E` has no `Dynamic` arm — the carrier {#regrowable_carrier?} refuses.
279
+ def precise_array?(nominal)
280
+ nominal.class_name == "Array" && nominal.type_args.size == 1 && !gradual_parameter?(nominal.type_args.first)
281
+ end
282
+
283
+ # Issue #2361 — a PRECISE `Array[E]` re-grows under a store, on the literal arms' terms: `a = ARGV.dup;
284
+ # a.clear; a << 1; a.each { |v| v.fdiv(2) }` read `Array[String]` and reported `undefined method 'fdiv' for
285
+ # String` on code that prints `[0.5]`.
286
+ #
287
+ # Only a content adder ({ContentJoin::ARRAY_CONTENT_ADDERS}) re-grows it, and the join keeps the seed: `clear`
288
+ # empties the array but leaves `E` standing, a superset of what it holds. A remover leaves the carrier as it
289
+ # found it. An element rewriter (`map!`, `collect!`, `flatten!`) replaces `E` (issue #2427): with the element
290
+ # the non-bang counterpart answers when the call site could type it ({RewriteMutation.typed}, answered before
291
+ # this method), and otherwise with {RewriteMutation.arm}'s gradual element — `a.map!.with_index { … }` holds
292
+ # whatever the block returns, and keeping the stale `E` reported a missing method on correct code.
293
+ #
294
+ # Each added member the element accepts joins as itself; any other takes the `Dynamic[top]` floor
295
+ # ({#precise_admitted}), and the floor joins every re-grow as it does on the literal arms. `a << 1` therefore
296
+ # reads `Array[Dynamic[top] | String]` and `s = ints.map { "x" }; s << "y"` reads `Array["x" | Dynamic[top]]`.
297
+ # Nothing narrows a carrier back when a remover or rewriter takes the store out again (`a << 1; a.pop`,
298
+ # `a << nil; a.compact!`, `a.map!(&:to_s)`), so a joined `Integer` or `nil` made `t.take(a)` against a declared
299
+ # `Array[String]` report `call.argument-type-mismatch`, and a parameter returned as its declared
300
+ # `-> Array[String]` report `def.return-type-mismatch`, on correct Ruby. A union carrying the floor neither folds
301
+ # nor dispatches loudly, so the read stays silent. Once floored, the carrier is gradual and later stores re-join
302
+ # through {#widen_nominal}'s ordinary arm.
303
+ #
304
+ # A store the element already accepts changes nothing ({#already_held?}). An adder with no argument types at
305
+ # all is left alone, as {MutationWidening.join_added_elements} leaves it: the block-capture seams pass none,
306
+ # and their own content join adds the stores.
307
+ def widen_precise_array(nominal, method_name, values:, arg_types:)
308
+ return RewriteMutation.arm(nominal, method_name) if RewriteMutation::TYPED_COUNTERPARTS.key?(method_name)
309
+ return nil unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
310
+
311
+ stored = ContentJoin.array_added_elements(method_name, arg_types)
312
+ return nil if already_held?(nominal.type_args.first, stored)
313
+
314
+ added = MutationWidening.value_pin_widened(stored)
315
+ admitted = precise_admitted(nominal.type_args.first, added)
316
+ pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
317
+ rejoined = ContentJoin.join_array_content(pre_state, MutationWidening.gradual_floor(admitted))
318
+ rejoined == nominal ? nil : rejoined
319
+ end
320
+
321
+ # The seed-admissibility gate for a precise element: an added member joins as itself only when `element`
322
+ # already accepts it, and takes the `Dynamic[top]` floor otherwise. A precise element therefore never gains a
323
+ # member it rejects, which is what a declared boundary checks: nothing narrows the carrier back when a remover
324
+ # or rewriter takes the store out again, and a joined foreign arm stays rejected beside the floor — `Integer`
325
+ # under a declared `Array[String]`, `String` under `-> Array["x"]`, `Array[Integer]` under
326
+ # `Array[Array[String]]` each reported on correct Ruby. A class set ({ContentJoin.admissible_evidence}) is too
327
+ # coarse for that: it admits `Array[Integer]` beside `[String]` and `"y"` beside `"x"`.
328
+ def precise_admitted(element, added)
329
+ added.flat_map do |type|
330
+ ContentJoin.union_members(type).map do |member|
331
+ Acceptance.accepts(element, member).yes? ? member : Type::Combinator.untyped
332
+ end
333
+ end
334
+ end
335
+
336
+ # True when the precise element `element` already covers every type `stored` puts in, so the store changes
337
+ # nothing: `s = ints.map { "x" }; s << "x"` keeps `Array["x"]` and its `s.last == "x"` fold, which is correct,
338
+ # and a `[Integer, Integer]` pushed onto an `Array[[0 | Integer, Integer]]` keeps the Tuple element. Joining
339
+ # the value-pin-widened, shape-erased store instead would only lose precision.
340
+ #
341
+ # A stored type carrying `Dynamic` anywhere is never covered, though `Acceptance` passes it gradually: keeping
342
+ # the element would claim the unknown part is what the element says (`retry_binding_accepted?`'s rule).
343
+ def already_held?(element, stored)
344
+ return false if stored.empty?
345
+
346
+ held = ContentJoin.union_members(element)
347
+ stored.all? do |type|
348
+ next false if type.nil? || carries_dynamic?(type)
349
+
350
+ ContentJoin.union_members(type).all? { |member| held.include?(member) } ||
351
+ Acceptance.accepts(element, type).yes?
352
+ end
353
+ end
354
+
355
+ def carries_dynamic?(type)
356
+ case type
357
+ when Type::Dynamic then true
358
+ when Type::Union, Type::Intersection then type.members.any? { |member| carries_dynamic?(member) }
359
+ when Type::Nominal then type.type_args.any? { |arg| carries_dynamic?(arg) }
360
+ when Type::Tuple then type.elements.any? { |element| carries_dynamic?(element) }
361
+ when Type::HashShape then type.pairs.each_value.any? { |value| carries_dynamic?(value) }
362
+ when Type::Difference, Type::Refined then carries_dynamic?(type.base)
363
+ else false
364
+ end
365
+ end
366
+
131
367
  # The value-rewriting counterpart of {MutationWidening.widen_tuple}'s `values: :widen`. A mutator that lands a
132
368
  # new value in an EXISTING slot falsifies that slot's pinning on a re-opened carrier exactly as
133
369
  # it does on a literal one, so `d = [:multi]; d << x; d[0] = "s"` must not keep reading `:multi`
@@ -7,9 +7,13 @@ require_relative "../source/node_children"
7
7
  require_relative "content_join"
8
8
  require_relative "hash_lookup_mutation"
9
9
  require_relative "mutation_rejoin"
10
+ require_relative "deep_erasure"
10
11
  require_relative "receiver_alias"
11
12
  require_relative "refinement_mutation"
13
+ # `RemoverNarrowing` is loaded with the engine (`scope.rb`), not here: the engine-free value layer requires this file
14
+ # and must not pull in the remover narrowing and its holder scan (`spec/rigor/cli/effects_cache_probe_spec.rb`).
12
15
  require_relative "rewrite_mutation"
16
+ require_relative "set_mutation"
13
17
  require_relative "string_mutation"
14
18
 
15
19
  module Rigor
@@ -18,8 +22,7 @@ module Rigor
18
22
  # AND whose method is a known in-place mutator.
19
23
  #
20
24
  # Closes the **G1 / G2** flow-folding gaps documented at
21
- # `docs/notes/20260521-mastodon-cluster4-flow-folding-triage.md` and queued in
22
- # [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding". The user-visible
25
+ # `docs/notes/20260521-mastodon-cluster4-flow-folding-triage.md`. The user-visible
23
26
  # symptom they shared was a spurious `flow.always-truthy-condition` on a `arr.size == N` /
24
27
  # `arr.empty?` / `@arr.empty?` check that follows a loop body or sibling method that mutates
25
28
  # `arr` / `@arr` in place.
@@ -75,6 +78,7 @@ module Rigor
75
78
  # ARE also handled** (ivars live in the method-body scope, not the block-local scope).
76
79
  #
77
80
  # The remaining three items above are demand-gated; see ADR-56.
81
+ # rubocop:disable-next Metrics/ModuleLength
78
82
  module MutationWidening
79
83
  # Array mutators that change either the size or the element set of a literal-shape carrier
80
84
  # (Tuple). Receiver-mutating methods only — non-mutating siblings (`map` vs `map!`,
@@ -106,7 +110,8 @@ module Rigor
106
110
  # Every method name {#widen_for_mutator} responds to — the one set a body scan asks "could an
107
111
  # in-place call on this name change its binding?" against (issue #587: the block-return
108
112
  # threading gate). Derived from the tables rather than spelled out, so the scan and the
109
- # widening it predicts cannot drift apart.
113
+ # widening it predicts cannot drift apart. The Set-only names ({SetMutation::MUTATORS}) stay out: the scans read
114
+ # a name without its receiver, and `merge` on a Hash returns a new Hash (issue #2294).
110
115
  SHAPE_MUTATORS = (ARRAY_MUTATORS | HASH_MUTATORS | HashLookupMutation::MUTATORS | StringMutation::MUTATORS).freeze
111
116
 
112
117
  # Methods that return the receiver (or a shallow copy) and cannot mutate it. They must not
@@ -133,10 +138,61 @@ module Rigor
133
138
  # pre-join behaviour exactly.
134
139
  NO_ARG_TYPES = [].freeze
135
140
 
136
- def widen_after_call(call_node:, current_scope:, arg_types: NO_ARG_TYPES)
141
+ # `rewritten_element` is the element a typed element rewriter stores ({RewriteMutation.typed}), or `nil`.
142
+ def widen_after_call(call_node:, current_scope:, arg_types: NO_ARG_TYPES, rewritten_element: nil)
137
143
  return current_scope if pure_self_returner?(call_node.name)
138
144
 
139
- widen_receiver_aliases(call_node.receiver, call_node.name, current_scope, arg_types: arg_types)
145
+ if RemoverNarrowing.candidate?(call_node)
146
+ narrowed = narrow_remover_receiver(call_node, current_scope, arg_types)
147
+ return narrowed if narrowed
148
+ end
149
+
150
+ widen_receiver_aliases(call_node.receiver, call_node.name, current_scope, arg_types:, rewritten_element:,
151
+ removes_nil: removes_nil?(call_node))
152
+ end
153
+
154
+ # Issue #2608 — a remover that narrows its receiver by what it removes ({RemoverNarrowing}): the receiver's
155
+ # binding widens as any mutator's does, then loses what the call provably took out. This answers before the
156
+ # #2555 remover floor, so a synthesised carrier a store grew keeps its precise reading (`parts << maybe_nil;
157
+ # parts.compact!` reads `Array[String]`). `nil` when the call narrows nothing; the caller then takes the
158
+ # ordinary path, the floor included.
159
+ def narrow_remover_receiver(call_node, scope, arg_types)
160
+ read = call_node.receiver
161
+ getter, builder = ALIAS_ACCESSORS[read.class]
162
+ name = ReceiverAlias.read_name(read)
163
+ current = scope.public_send(getter, name)
164
+ return nil if current.nil? || RemoverHolderScan.unsafe_receiver?(scope, name)
165
+
166
+ narrowed = RemoverNarrowing.narrow_shape(current, call_node)
167
+ if narrowed.nil?
168
+ synthesised = synthesised_binding?(scope, getter, name)
169
+ base = widen_for_mutator(current, call_node.name, arg_types:, synthesised:)
170
+ narrowed = RemoverNarrowing.narrow(base || current, call_node, scope)
171
+ end
172
+ return nil if narrowed.nil?
173
+ return scope if narrowed == current
174
+
175
+ MutationRejoin.rebind(scope, builder, name, narrowed, pre_state: current, kind: getter)
176
+ end
177
+
178
+ # Issue #2555 — the removers that may take a `nil` out, which is every one but a `delete` / `delete?` given a
179
+ # non-nil literal: false positives outrank, so a positional remover (`pop`, `shift`, `slice!`), `subtract`
180
+ # (whose argument may hold `nil`) and a `delete` of an unknown value count. `Hash#delete` removes by key, so
181
+ # whatever value the key held may go ({.floor_grown_carrier} reads the receiver for it).
182
+ NIL_REMOVERS = %i[compact! reject! select! filter! delete_if keep_if clear pop shift slice!
183
+ subtract].to_set.freeze
184
+ NON_NIL_LITERALS = [
185
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode, Prism::StringNode,
186
+ Prism::SymbolNode, Prism::TrueNode, Prism::FalseNode
187
+ ].freeze
188
+ private_constant :NON_NIL_LITERALS
189
+
190
+ def removes_nil?(call_node)
191
+ return true if NIL_REMOVERS.include?(call_node.name)
192
+ return false unless %i[delete delete?].include?(call_node.name)
193
+
194
+ argument = call_node.arguments&.arguments&.first
195
+ NON_NIL_LITERALS.none? { |klass| argument.is_a?(klass) }
140
196
  end
141
197
 
142
198
  # True when `receiver` names at least one variable whose CURRENT binding is a literal-shape
@@ -147,10 +203,19 @@ module Rigor
147
203
 
148
204
  ReceiverAlias.mutated_reads(receiver).any? do |read|
149
205
  getter, = ALIAS_ACCESSORS[read.class]
150
- getter && shape_carrier?(scope.public_send(getter, ReceiverAlias.read_name(read)))
206
+ next false if getter.nil?
207
+
208
+ name = ReceiverAlias.read_name(read)
209
+ shape_carrier?(scope.public_send(getter, name), synthesised: synthesised_binding?(scope, getter, name))
151
210
  end
152
211
  end
153
212
 
213
+ # Issue #2555 L3 — whether the binding `getter` / `name` reads carries the `:synthesised` construction fact
214
+ # ({Scope#construction_fact?}). Only a local can: an instance, class or global variable never carries one.
215
+ def synthesised_binding?(scope, getter, name)
216
+ getter == :local && scope.construction_fact?(name)
217
+ end
218
+
154
219
  # A union carrying a shape member is joinable too (issue #645): {#widen_union} widens that
155
220
  # member through the same join, so the arguments the caller would skip are the evidence it
156
221
  # needs.
@@ -159,26 +224,32 @@ module Rigor
159
224
  # An empty-witness `Difference` counts too (issue #936): the `non-empty-array[T]` arm joins the
160
225
  # mutator's added element now, so its arguments are evidence this seam consumes rather than
161
226
  # arguments nothing will read.
162
- def shape_carrier?(type)
227
+ # A precise `Hash` counts too when its binding carries the `:synthesised` construction fact (issue #2555): the
228
+ # re-join grows it from these arguments.
229
+ def shape_carrier?(type, synthesised: false)
163
230
  case type
164
231
  when Type::Tuple, Type::HashShape then true
165
- when Type::Constant then StringMutation.constant?(type)
232
+ when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
166
233
  # A re-openable nominal counts since issue #580: skipping it made the one-way door
167
234
  # self-fulfilling, since the arm never saw the arg types it grows from.
168
- when Type::Nominal then MutationRejoin.regrowable_carrier?(type)
235
+ # A precise `Array[E]` / `Set[E]` counts since issue #2361, which re-grows it from these arguments.
236
+ when Type::Nominal
237
+ MutationRejoin.regrowable_carrier?(type) || MutationRejoin.precise_array?(type) ||
238
+ SetMutation.regrowable?(type) || (synthesised && MutationRejoin.synthesised_carrier?(type))
169
239
  when Type::Difference then type.removes_empty_witness?
170
- when Type::Union then type.members.any? { |m| shape_carrier?(m) }
240
+ when Type::Union then type.members.any? { |m| shape_carrier?(m, synthesised:) }
171
241
  else false
172
242
  end
173
243
  end
174
244
 
175
245
  # Widens every variable `receiver` can evaluate to ({ReceiverAlias.mutated_reads}), against `method_name`'s
176
246
  # mutator table.
177
- def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES)
247
+ def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES, rewritten_element: nil,
248
+ removes_nil: false)
178
249
  return current_scope if receiver.nil?
179
250
 
180
251
  ReceiverAlias.mutated_reads(receiver).reduce(current_scope) do |acc, read|
181
- widen_alias_read(method_name, read, acc, arg_types: arg_types)
252
+ widen_alias_read(method_name, read, acc, arg_types:, rewritten_element:, removes_nil:)
182
253
  end
183
254
  end
184
255
 
@@ -195,17 +266,19 @@ module Rigor
195
266
  # always safe — it can only LOSE precision — so blindly propagating is sound regardless of
196
267
  # whether the block actually runs.
197
268
  #
198
- # Recurses into nested expression nodes so chained / nested forms (`arr << f(x); arr <<
199
- # g(y)`, `arr.push(x) if cond`) are all caught. Does NOT recurse into nested
200
- # `Prism::BlockNode`s — each block is processed by its own `eval_call`.
269
+ # Recurses into every nested node, blocks included, so chained / nested forms (`arr << f(x); arr <<
270
+ # g(y)`, `arr.push(x) if cond`) are all caught.
201
271
  def widen_after_block(call_node:, outer_scope:)
202
272
  block = call_node.block
273
+ return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
203
274
  return outer_scope unless block.is_a?(Prism::BlockNode)
204
275
 
205
276
  body = block.body
206
277
  return outer_scope if body.nil?
207
278
 
208
- walk_for_outer_mutations(body, outer_scope)
279
+ scope = walk_for_outer_mutations(body, outer_scope)
280
+ names = DeepErasure.parameter_names(block.parameters)
281
+ DeepErasure.widen_element_aliases(call_node.receiver, block.body, names, scope)
209
282
  end
210
283
 
211
284
  def walk_for_outer_mutations(node, scope)
@@ -231,6 +304,9 @@ module Rigor
231
304
  receiver = call_node.receiver
232
305
  return scope if receiver.nil?
233
306
 
307
+ widened = widen_outer_element_read(call_node, scope)
308
+ return widened if widened
309
+
234
310
  ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
235
311
  # A block-local read ({ReceiverAlias.block_local?}: a `depth == 0` local, or `it`) is not a
236
312
  # capture of the outer scope, so widening its name against the OUTER scope would hit an
@@ -241,10 +317,27 @@ module Rigor
241
317
  # after it re-adds the appended values' types onto the widened seed, so the seed's own value
242
318
  # pinning is still justified (`out = [0]; arr.each { out << x }` never rewrites slot 0). The
243
319
  # straight-line paths have no such join, which is why their default is `:widen` (issue #560).
244
- widen_alias_read(call_node.name, read, acc, values: :keep)
320
+ widen_alias_read(call_node.name, read, acc, values: :keep, unread_arguments: !call_node.arguments.nil?)
245
321
  end
246
322
  end
247
323
 
324
+ # Issue #2287 — a mutator on an element read rooted at a captured local (`[0].each { |i| a[i] << 2 }`,
325
+ # `loop { a[0] << 2; break }`) mutates an element the outer binding still carries, and names no variable
326
+ # {ReceiverAlias.candidates} could widen. The element at that path widens in the outer scope as the
327
+ # straight-line {ElementReadWidening.widen_element_read} widens it; a block-local root is skipped as a
328
+ # block-local read is. Answers the widened scope, or `nil` when the receiver is no such read or nothing widened.
329
+ def widen_outer_element_read(call_node, scope)
330
+ path = ElementReadWidening.element_read_path(call_node.receiver)
331
+ return nil if path.nil?
332
+
333
+ read, steps = path
334
+ return nil if ReceiverAlias.block_local?(read)
335
+
336
+ current = scope.local(read.name)
337
+ widened = current && ElementReadWidening.widen_through_path(current, steps, call_node.name, NO_ARG_TYPES)
338
+ widened && scope.with_mutated_local(read.name, widened)
339
+ end
340
+
248
341
  # The arms per kind of binding differ only in which pair of `Scope` accessors they use, so they are read out
249
342
  # of one table rather than written once per kind: a carrier the widening responds to must not be widened on
250
343
  # one kind of binding and left on another. `$g << x; $g == "k"` folded always-truthy on a `$g` Ruby holds
@@ -257,18 +350,38 @@ module Rigor
257
350
  Prism::ClassVariableReadNode => %i[cvar with_cvar],
258
351
  Prism::GlobalVariableReadNode => %i[global with_global]
259
352
  })
260
- private_constant :ALIAS_ACCESSORS
261
353
 
262
- def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
354
+ # `unread_arguments` says the call passes arguments this seam did not type (the block-capture path): a Set,
355
+ # which has no block content join, takes one unknown store for it ({SetMutation.widen}).
356
+ def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false, # rubocop:disable Metrics/ParameterLists
357
+ rewritten_element: nil, removes_nil: false)
263
358
  getter, builder = ALIAS_ACCESSORS[read.class]
264
359
  return scope if getter.nil?
265
360
 
266
361
  name = ReceiverAlias.read_name(read)
267
362
  current = scope.public_send(getter, name)
268
- widened = widen_for_mutator(current, method_name, values: values, arg_types: arg_types)
269
- return scope if widened.nil?
363
+ synthesised = synthesised_binding?(scope, getter, name)
364
+ if synthesised && MutationRejoin.floors_grown_carrier?(scope, name, method_name)
365
+ return floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
366
+ end
270
367
 
271
- MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
368
+ widened = widen_for_mutator(current, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
369
+ synthesised:)
370
+ scope = MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter) if widened
371
+ return scope unless synthesised && MutationRejoin.content_adder?(method_name)
372
+
373
+ MutationRejoin.grew_unfloored?(current, widened) ? scope.with_construction_fact(name, :grown) : scope
374
+ end
375
+
376
+ # Issue #2555 — a remover or filter on a synthesised carrier a store has grown. Nothing narrows the carrier back,
377
+ # so the stored arm would outlive the values the call removes (`parts << nil; parts.compact!`); the carrier takes
378
+ # the `Dynamic[top]` floor it would have carried without the fact. The binding keeps its facts: the floored
379
+ # carrier is gradual, so the synthesised re-join no longer applies to it, and a later remover floors it again.
380
+ def floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
381
+ drop_nil = removes_nil || (method_name == :delete && MutationRejoin.hash_carrier?(current))
382
+ floored = MutationRejoin.floor_parameters(current, drop_nil:)
383
+ floored = widen_for_mutator(floored, method_name) || floored
384
+ MutationRejoin.rebind(scope, builder, name, floored, pre_state: current, kind: getter)
272
385
  end
273
386
 
274
387
  # Mutators that can land a NEW value in an EXISTING slot, falsifying that slot's value pinning
@@ -282,15 +395,32 @@ module Rigor
282
395
  # Returns the widened type for a binding whose receiver is about to be mutated by
283
396
  # `method_name`, or `nil` when no widening applies (binding is not a carrier the mutation
284
397
  # stands for, OR the method is not a mutator for that shape, OR the carrier is a PRECISE
285
- # nominal, whose element set is a claim this seam may not grow — see {MutationRejoin.regrowable_carrier?}).
286
- def widen_for_mutator(type, method_name, values: :widen, arg_types: NO_ARG_TYPES)
398
+ # nominal the call stores nothing into — see {MutationRejoin.widen_precise_array}).
399
+ # A `rewritten_element` answers first for the carriers {RewriteMutation.typed} types (issue #2427).
400
+ # `synthesised` says the binding carries the `:synthesised` construction fact (issue #2555,
401
+ # {Scope#construction_fact?}): a precise nominal or Set it holds re-grows without the `Dynamic[top]` floor
402
+ # ({MutationRejoin.widen_synthesised}, {SetMutation.widen}).
403
+ def widen_for_mutator(type, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
404
+ rewritten_element: nil, synthesised: false)
405
+ typed = rewritten_element && type && RewriteMutation.typed(type, method_name, rewritten_element)
406
+ return typed == type ? nil : typed if typed
407
+
408
+ widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
409
+ end
410
+
411
+ # {#widen_for_mutator} for a carrier no typed element rewrite answered.
412
+ def widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
413
+ synthesised: false)
287
414
  values = :keep unless VALUE_REWRITING_MUTATORS.include?(method_name)
288
415
  values = RewriteMutation.values_mode(type, method_name, values)
289
416
 
290
417
  return nil if type.nil?
418
+ if SetMutation.set?(type)
419
+ return SetMutation.widen(type, method_name, arg_types, unread_arguments:, synthesised:)
420
+ end
291
421
 
292
422
  case type
293
- when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:)
423
+ when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:, synthesised:)
294
424
  when Type::Tuple
295
425
  return nil unless ARRAY_MUTATORS.include?(method_name)
296
426
 
@@ -304,7 +434,7 @@ module Rigor
304
434
  when Type::Difference
305
435
  widen_difference(type, method_name, arg_types: arg_types)
306
436
  when Type::Union
307
- widen_union(type, method_name, values: values, arg_types: arg_types)
437
+ widen_union(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
308
438
  end
309
439
  end
310
440
 
@@ -322,12 +452,14 @@ module Rigor
322
452
  #
323
453
  # `nil` when no member widened — a union with no carrier member, or one whose carriers are
324
454
  # already nominal, is untouched exactly as the scalar path leaves them.
325
- def widen_union(union, method_name, values: :widen, arg_types: NO_ARG_TYPES)
455
+ def widen_union(union, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
456
+ rewritten_element: nil, synthesised: false)
326
457
  widened_any = false
327
458
  members = union.members.map do |member|
328
459
  next member unless mutation_carrier?(member, method_name)
329
460
 
330
- widened = widen_for_mutator(member, method_name, values: values, arg_types: arg_types)
461
+ widened = widen_for_mutator(member, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
462
+ synthesised:)
331
463
  next member if widened.nil?
332
464
 
333
465
  widened_any = true
@@ -345,7 +477,8 @@ module Rigor
345
477
  (ARRAY_MUTATORS.include?(method_name) && ContentJoin.array_residue(member).empty?) ||
346
478
  ((HASH_MUTATORS.include?(method_name) || HashLookupMutation::MUTATORS.include?(method_name)) &&
347
479
  ContentJoin.hash_residue(member).empty?) ||
348
- (StringMutation::MUTATORS.include?(method_name) && StringMutation.constant?(member))
480
+ (StringMutation::MUTATORS.include?(method_name) && StringMutation.constant?(member)) ||
481
+ (SetMutation::MUTATORS.include?(method_name) && SetMutation.carrier?(member))
349
482
  end
350
483
 
351
484
  # Joins the element evidence the mutator's own ARGUMENTS introduce into the already-widened