rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -10,7 +10,10 @@ require_relative "mutation_rejoin"
10
10
  require_relative "deep_erasure"
11
11
  require_relative "receiver_alias"
12
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`).
13
15
  require_relative "rewrite_mutation"
16
+ require_relative "set_mutation"
14
17
  require_relative "string_mutation"
15
18
 
16
19
  module Rigor
@@ -19,8 +22,7 @@ module Rigor
19
22
  # AND whose method is a known in-place mutator.
20
23
  #
21
24
  # Closes the **G1 / G2** flow-folding gaps documented at
22
- # `docs/notes/20260521-mastodon-cluster4-flow-folding-triage.md` and queued in
23
- # [`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
24
26
  # symptom they shared was a spurious `flow.always-truthy-condition` on a `arr.size == N` /
25
27
  # `arr.empty?` / `@arr.empty?` check that follows a loop body or sibling method that mutates
26
28
  # `arr` / `@arr` in place.
@@ -76,6 +78,7 @@ module Rigor
76
78
  # ARE also handled** (ivars live in the method-body scope, not the block-local scope).
77
79
  #
78
80
  # The remaining three items above are demand-gated; see ADR-56.
81
+ # rubocop:disable-next Metrics/ModuleLength
79
82
  module MutationWidening
80
83
  # Array mutators that change either the size or the element set of a literal-shape carrier
81
84
  # (Tuple). Receiver-mutating methods only — non-mutating siblings (`map` vs `map!`,
@@ -107,7 +110,8 @@ module Rigor
107
110
  # Every method name {#widen_for_mutator} responds to — the one set a body scan asks "could an
108
111
  # in-place call on this name change its binding?" against (issue #587: the block-return
109
112
  # threading gate). Derived from the tables rather than spelled out, so the scan and the
110
- # 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).
111
115
  SHAPE_MUTATORS = (ARRAY_MUTATORS | HASH_MUTATORS | HashLookupMutation::MUTATORS | StringMutation::MUTATORS).freeze
112
116
 
113
117
  # Methods that return the receiver (or a shallow copy) and cannot mutate it. They must not
@@ -134,10 +138,61 @@ module Rigor
134
138
  # pre-join behaviour exactly.
135
139
  NO_ARG_TYPES = [].freeze
136
140
 
137
- 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)
138
143
  return current_scope if pure_self_returner?(call_node.name)
139
144
 
140
- 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) }
141
196
  end
142
197
 
143
198
  # True when `receiver` names at least one variable whose CURRENT binding is a literal-shape
@@ -148,10 +203,19 @@ module Rigor
148
203
 
149
204
  ReceiverAlias.mutated_reads(receiver).any? do |read|
150
205
  getter, = ALIAS_ACCESSORS[read.class]
151
- 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))
152
210
  end
153
211
  end
154
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
+
155
219
  # A union carrying a shape member is joinable too (issue #645): {#widen_union} widens that
156
220
  # member through the same join, so the arguments the caller would skip are the evidence it
157
221
  # needs.
@@ -160,26 +224,32 @@ module Rigor
160
224
  # An empty-witness `Difference` counts too (issue #936): the `non-empty-array[T]` arm joins the
161
225
  # mutator's added element now, so its arguments are evidence this seam consumes rather than
162
226
  # arguments nothing will read.
163
- 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)
164
230
  case type
165
231
  when Type::Tuple, Type::HashShape then true
166
- when Type::Constant then StringMutation.constant?(type)
232
+ when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
167
233
  # A re-openable nominal counts since issue #580: skipping it made the one-way door
168
234
  # self-fulfilling, since the arm never saw the arg types it grows from.
169
- 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))
170
239
  when Type::Difference then type.removes_empty_witness?
171
- when Type::Union then type.members.any? { |m| shape_carrier?(m) }
240
+ when Type::Union then type.members.any? { |m| shape_carrier?(m, synthesised:) }
172
241
  else false
173
242
  end
174
243
  end
175
244
 
176
245
  # Widens every variable `receiver` can evaluate to ({ReceiverAlias.mutated_reads}), against `method_name`'s
177
246
  # mutator table.
178
- 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)
179
249
  return current_scope if receiver.nil?
180
250
 
181
251
  ReceiverAlias.mutated_reads(receiver).reduce(current_scope) do |acc, read|
182
- 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:)
183
253
  end
184
254
  end
185
255
 
@@ -196,9 +266,8 @@ module Rigor
196
266
  # always safe — it can only LOSE precision — so blindly propagating is sound regardless of
197
267
  # whether the block actually runs.
198
268
  #
199
- # Recurses into nested expression nodes so chained / nested forms (`arr << f(x); arr <<
200
- # g(y)`, `arr.push(x) if cond`) are all caught. Does NOT recurse into nested
201
- # `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.
202
271
  def widen_after_block(call_node:, outer_scope:)
203
272
  block = call_node.block
204
273
  return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
@@ -235,6 +304,9 @@ module Rigor
235
304
  receiver = call_node.receiver
236
305
  return scope if receiver.nil?
237
306
 
307
+ widened = widen_outer_element_read(call_node, scope)
308
+ return widened if widened
309
+
238
310
  ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
239
311
  # A block-local read ({ReceiverAlias.block_local?}: a `depth == 0` local, or `it`) is not a
240
312
  # capture of the outer scope, so widening its name against the OUTER scope would hit an
@@ -245,10 +317,27 @@ module Rigor
245
317
  # after it re-adds the appended values' types onto the widened seed, so the seed's own value
246
318
  # pinning is still justified (`out = [0]; arr.each { out << x }` never rewrites slot 0). The
247
319
  # straight-line paths have no such join, which is why their default is `:widen` (issue #560).
248
- 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?)
249
321
  end
250
322
  end
251
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
+
252
341
  # The arms per kind of binding differ only in which pair of `Scope` accessors they use, so they are read out
253
342
  # of one table rather than written once per kind: a carrier the widening responds to must not be widened on
254
343
  # one kind of binding and left on another. `$g << x; $g == "k"` folded always-truthy on a `$g` Ruby holds
@@ -262,16 +351,37 @@ module Rigor
262
351
  Prism::GlobalVariableReadNode => %i[global with_global]
263
352
  })
264
353
 
265
- 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)
266
358
  getter, builder = ALIAS_ACCESSORS[read.class]
267
359
  return scope if getter.nil?
268
360
 
269
361
  name = ReceiverAlias.read_name(read)
270
362
  current = scope.public_send(getter, name)
271
- widened = widen_for_mutator(current, method_name, values: values, arg_types: arg_types)
272
- 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
273
367
 
274
- 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)
275
385
  end
276
386
 
277
387
  # Mutators that can land a NEW value in an EXISTING slot, falsifying that slot's value pinning
@@ -285,15 +395,32 @@ module Rigor
285
395
  # Returns the widened type for a binding whose receiver is about to be mutated by
286
396
  # `method_name`, or `nil` when no widening applies (binding is not a carrier the mutation
287
397
  # stands for, OR the method is not a mutator for that shape, OR the carrier is a PRECISE
288
- # nominal, whose element set is a claim this seam may not grow — see {MutationRejoin.regrowable_carrier?}).
289
- 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)
290
414
  values = :keep unless VALUE_REWRITING_MUTATORS.include?(method_name)
291
415
  values = RewriteMutation.values_mode(type, method_name, values)
292
416
 
293
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
294
421
 
295
422
  case type
296
- 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:)
297
424
  when Type::Tuple
298
425
  return nil unless ARRAY_MUTATORS.include?(method_name)
299
426
 
@@ -307,7 +434,7 @@ module Rigor
307
434
  when Type::Difference
308
435
  widen_difference(type, method_name, arg_types: arg_types)
309
436
  when Type::Union
310
- 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:)
311
438
  end
312
439
  end
313
440
 
@@ -325,12 +452,14 @@ module Rigor
325
452
  #
326
453
  # `nil` when no member widened — a union with no carrier member, or one whose carriers are
327
454
  # already nominal, is untouched exactly as the scalar path leaves them.
328
- 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)
329
457
  widened_any = false
330
458
  members = union.members.map do |member|
331
459
  next member unless mutation_carrier?(member, method_name)
332
460
 
333
- 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:)
334
463
  next member if widened.nil?
335
464
 
336
465
  widened_any = true
@@ -348,7 +477,8 @@ module Rigor
348
477
  (ARRAY_MUTATORS.include?(method_name) && ContentJoin.array_residue(member).empty?) ||
349
478
  ((HASH_MUTATORS.include?(method_name) || HashLookupMutation::MUTATORS.include?(method_name)) &&
350
479
  ContentJoin.hash_residue(member).empty?) ||
351
- (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))
352
482
  end
353
483
 
354
484
  # Joins the element evidence the mutator's own ARGUMENTS introduce into the already-widened
@@ -608,6 +608,26 @@ module Rigor
608
608
  truthy
609
609
  end
610
610
 
611
+ # Issue #2443 — `scope` with what every match binds, laid over the bindings it holds, for the block of a call
612
+ # that yields only after it has matched in the caller's frame ({MatchRebinding::YieldingMatch}): `$~` a
613
+ # `MatchData`, and `$&` and the pre- and post-match a `String`. The numbered groups and `$+` keep the bindings
614
+ # `scope` gives them. Typing them from the call's own pattern needs the group walk
615
+ # ({#unconditional_capture_groups}) to read every source right, which it does not yet (#2472), and forgetting
616
+ # them would report a group an earlier match in the method narrowed.
617
+ def yielded_match_scope(scope)
618
+ string_t = Type::Combinator.nominal_of("String")
619
+ scope.with_global(:$~, Type::Combinator.nominal_of("MatchData"))
620
+ .with_global(:$&, string_t).with_global(:$`, string_t).with_global(:$', string_t)
621
+ end
622
+
623
+ # Issue #2443 — true when every member of `type` is a String (a project subclass included) or a `Dynamic` that
624
+ # is untyped or may hold only Strings, Symbols and nil ({#match_member_kind}). A `nil` member never runs the
625
+ # block, so it counts neither way.
626
+ def string_match_receiver?(type, scope)
627
+ kinds = match_member_kinds(type, scope) - [:nil]
628
+ !kinds.empty? && kinds.all? { |kind| %i[string string_subclass dynamic].include?(kind) }
629
+ end
630
+
611
631
  # Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
612
632
  # recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
613
633
  # entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
@@ -968,12 +988,22 @@ module Rigor
968
988
  end
969
989
 
970
990
  def analyse_local_read(node, scope)
971
- current = scope.local(node.name)
991
+ local_truthiness_edges(node.name, scope)
992
+ end
993
+
994
+ # The falsey edge is where an optimistic lookup's miss lands (ADR-101, issue #2306): `Scope#with_local`
995
+ # drops the issue #286 mark as a write does, so the edge re-takes the one `scope` holds. Without it the
996
+ # join after `if v; …; end` with no `else` lost the mark on both arms and read `v` as nil-free, although
997
+ # a miss reaches that join through the implicit else. The truthy edge proves the value non-nil, so it
998
+ # keeps dropping the mark.
999
+ def local_truthiness_edges(name, scope)
1000
+ current = scope.local(name)
972
1001
  return nil if current.nil?
973
1002
 
1003
+ falsey = scope.with_local(name, narrow_falsey(current))
974
1004
  [
975
- scope.with_local(node.name, narrow_truthy(current)),
976
- scope.with_local(node.name, narrow_falsey(current))
1005
+ scope.with_local(name, narrow_truthy(current)),
1006
+ OptimisticOrigin.carry_local_mark(scope, falsey, name, nil)
977
1007
  ]
978
1008
  end
979
1009
 
@@ -987,13 +1017,7 @@ module Rigor
987
1017
  # holds the just-bound value; the narrowing contract on the surrounding `if` is the
988
1018
  # same.
989
1019
  def analyse_local_write(node, scope)
990
- current = scope.local(node.name)
991
- return nil if current.nil?
992
-
993
- [
994
- scope.with_local(node.name, narrow_truthy(current)),
995
- scope.with_local(node.name, narrow_falsey(current))
996
- ]
1020
+ local_truthiness_edges(node.name, scope)
997
1021
  end
998
1022
 
999
1023
  # Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
@@ -1007,9 +1031,11 @@ module Rigor
1007
1031
  current = scope.ivar(node.name)
1008
1032
  return nil if current.nil?
1009
1033
 
1034
+ # The falsey edge keeps the optimistic mark, as {#local_truthiness_edges} explains.
1035
+ falsey = scope.with_ivar(node.name, narrow_falsey(current))
1010
1036
  [
1011
1037
  scope.with_ivar(node.name, narrow_truthy(current)),
1012
- scope.with_ivar(node.name, narrow_falsey(current))
1038
+ OptimisticOrigin.carry_ivar_mark(scope, falsey, node.name, nil)
1013
1039
  ]
1014
1040
  end
1015
1041
 
@@ -1180,15 +1206,6 @@ module Rigor
1180
1206
  refinements
1181
1207
  end
1182
1208
 
1183
- # Recognised CallNode predicates:
1184
- # - `recv.nil?` (Slice 6 phase 1, no args, no block)
1185
- # - unary `!recv` (`name == :!`, no args, no block)
1186
- # - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
1187
- # static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
1188
- # - `local == literal` / `literal == local` and the `!=` mirror for trusted static
1189
- # literals (Slice 6 phase 2 sub-phase 2).
1190
- # Anything else returns nil so the surrounding analyser falls through to the
1191
- # no-narrowing fallback.
1192
1209
  # Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
1193
1210
  # it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
1194
1211
  # variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
@@ -2466,10 +2483,10 @@ module Rigor
2466
2483
  narrowed == original ? :relational : :local_binding
2467
2484
  end
2468
2485
 
2469
- # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
2470
- # MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
2486
+ # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The argument
2471
2487
  # MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
2472
- # qualified class name. Anything else falls through to "no narrowing".
2488
+ # qualified class name. The receiver is a local, a global, constant or instance variable
2489
+ # ({#receiver_slot}) or a single-hop chain; anything else falls through to "no narrowing".
2473
2490
  def analyse_class_predicate(node, scope, exact:)
2474
2491
  return nil if node.arguments.nil?
2475
2492
  return nil unless node.arguments.arguments.size == 1
@@ -2538,14 +2555,13 @@ module Rigor
2538
2555
  class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
2539
2556
  end
2540
2557
 
2541
- # Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
2542
- # receiver narrowing across stable receivers"). When the predicate's receiver is
2558
+ # Stable single-hop method-chain narrowing. When the predicate's receiver is
2543
2559
  # `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
2544
2560
  # narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
2545
2561
  # dominated body's identical chain reads then observe the narrowed type through
2546
2562
  # `ExpressionTyper#call_type_for`'s lookup.
2547
2563
  #
2548
- # Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
2564
+ # Heuristic-by-design: a second call to `x.last` could in
2549
2565
  # principle return a different value than the first. The chain is dropped on (1)
2550
2566
  # receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
2551
2567
  # any intervening call against the same root receiver (handled by
@@ -2864,9 +2880,7 @@ module Rigor
2864
2880
  # - For each fact, the corresponding positional argument (matched by parameter name in
2865
2881
  # the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
2866
2882
  # apply.
2867
- # - When the target is `self`, narrowing applies to the receiver — but the engine does
2868
- # not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
2869
- # but currently produce no scope edits.
2883
+ # - When the target is `self`, narrowing applies to the receiver ({#apply_self_fact}).
2870
2884
  def analyse_rbs_extended_contribution(node, scope)
2871
2885
  method_def = resolve_rbs_extended_method(node, scope)
2872
2886
  return nil if method_def.nil?
@@ -3403,8 +3417,8 @@ module Rigor
3403
3417
  # extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
3404
3418
  # written in a file outside the analysed set are both invisible to the walk behind
3405
3419
  # {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
3406
- # complete — RBS says nothing about a class it does not declare. A shape that turned this table into a positive
3407
- # `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3420
+ # complete — RBS says nothing about a class it does not declare. A shape that turned this table
3421
+ # into a positive `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3408
3422
  # `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
3409
3423
  # it, trading the unreachable-clause false positive for an undefined-method one — the same trap
3410
3424
  # #657 recorded on the `Constant` carrier.
@@ -3586,7 +3600,7 @@ module Rigor
3586
3600
  rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
3587
3601
  end
3588
3602
 
3589
- # The four receiver shapes with a scope binding the edges can rebind, mirroring
3603
+ # The three receiver shapes with a scope binding the edges can rebind, mirroring
3590
3604
  # `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
3591
3605
  # the caller declines.
3592
3606
  def receiver_binding_type(receiver, scope)
@@ -3696,7 +3710,7 @@ module Rigor
3696
3710
 
3697
3711
  # Walks a call chain down to its `&.` link, collecting the suffix method names applied
3698
3712
  # above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
3699
- # direct local / ivar read — those are the two bindings a scope can rewrite — and unless
3713
+ # direct local, instance-variable, global or constant read — the slots a scope can rewrite — and unless
3700
3714
  # every suffix is a bare reader or predicate, since an argument or a block could make the
3701
3715
  # answer depend on something other than the receiver.
3702
3716
  #
@@ -18,7 +18,7 @@ module Rigor
18
18
  # that happens to have been produced optimistically.
19
19
  #
20
20
  # Issue #286: the `if` / `unless` branch elision is one consumer of `Narrowing.predicate_certainty`; the
21
- # other two are `flow.always-truthy-condition` and the `&&` / `||` `constant_value_polarity` gate, and the
21
+ # other two are `flow.always-truthy-condition` and `StatementEvaluator#right_operand_dead?`, and the
22
22
  # spec passage above binds all three. This channel is what lets a certainty judgment tell the two apart.
23
23
  #
24
24
  # Issue #313: the mark is attached to a *value* — the call node that produced it, or the local / ivar it
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "mutation_widening"
5
+ require_relative "receiver_alias"
6
+ require_relative "../type"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2499 rule 3 (part of #2494) — the stdlib calls that fill a String the caller passes in. After
11
+ # `io.read(n, buf)`, `io.readpartial(n, buf)`, `io.sysread(n, buf)` and `io.read_nonblock(n, buf)` the buffer
12
+ # holds what was read, and after `StringIO.new(s)` every later write through the StringIO lands in `s`. The
13
+ # argument (a local, instance, class or global variable read) is widened as an in-place `replace` of it would
14
+ # widen it, so a `buf = +""` seed stops reading `""` after the call.
15
+ #
16
+ # A built-in catalogue keyed by method name and argument position, not a receiver-typed dispatch: a method of
17
+ # another class with the same name and a second positional argument loses only precision on that argument.
18
+ module OutBufferMutation
19
+ # Method name => the zero-based position of the buffer argument.
20
+ READERS = { read: 1, readpartial: 1, sysread: 1, read_nonblock: 1 }.freeze
21
+ private_constant :READERS
22
+
23
+ module_function
24
+
25
+ # `scope` with the buffer argument of `call_node` widened, or `scope` itself when the call is no such call or the
26
+ # argument holds no String: `Rails.cache.read(key, opts)` passes an options Hash that keeps its shape.
27
+ def floor(call_node, scope)
28
+ buffer = buffer_argument(call_node)
29
+ return scope if buffer.nil?
30
+
31
+ getter, = MutationWidening::ALIAS_ACCESSORS[buffer.class]
32
+ return scope unless string_carrier?(scope.public_send(getter, ReceiverAlias.read_name(buffer)))
33
+
34
+ MutationWidening.widen_alias_read(:replace, buffer, scope)
35
+ end
36
+
37
+ # A `Constant<String>` or the `String` nominal, or a union of nothing else.
38
+ def string_carrier?(type)
39
+ case type
40
+ when Type::Constant then type.value.is_a?(String)
41
+ when Type::Nominal then type.class_name == "String"
42
+ when Type::Union then type.members.all? { |member| string_carrier?(member) }
43
+ else false
44
+ end
45
+ end
46
+
47
+ def buffer_argument(call_node)
48
+ arguments = call_node.arguments&.arguments
49
+ return nil if arguments.nil?
50
+
51
+ position = READERS[call_node.name]
52
+ position = 0 if position.nil? && string_io_new?(call_node)
53
+ return nil if position.nil?
54
+
55
+ argument = arguments[position]
56
+ MutationWidening::ALIAS_ACCESSORS.key?(argument.class) ? argument : nil
57
+ end
58
+
59
+ def string_io_new?(call_node)
60
+ return false unless call_node.name == :new
61
+
62
+ receiver = call_node.receiver
63
+ case receiver
64
+ when Prism::ConstantReadNode then receiver.name == :StringIO
65
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :StringIO
66
+ else false
67
+ end
68
+ end
69
+ private_class_method :buffer_argument, :string_io_new?, :string_carrier?
70
+ end
71
+ end
72
+ end
@@ -95,11 +95,10 @@ module Rigor
95
95
  # may type in a later round once its own argument resolves). Unresolved call sites do not
96
96
  # contribute.
97
97
  #
98
- # What it does NOT do yet (deferred — the check-wiring slice): feeding the table into the
99
- # `check` walk (only `coverage --protection` consumes it today), keyword / optional / rest
100
- # / block parameters, inherited-method receivers, top-level helpers, and a rigorous
101
- # closed-call-site-set proof (the union is optimistic over resolved sites — acceptable
102
- # because the only consumer is the protection metric, which runs no diagnostics).
98
+ # What it does NOT do yet: typing keyword / optional / rest / block parameters themselves,
99
+ # inherited-method receivers, top-level helpers, and a rigorous closed-call-site-set proof
100
+ # (the union is optimistic over resolved sites; the `check` walk tolerates that because an
101
+ # inferred parameter is only a lower bound, which the in-body rules decline on, WD6b).
103
102
  class ParameterInferenceCollector
104
103
  EMPTY = {}.freeze
105
104
 
@@ -120,7 +119,7 @@ module Rigor
120
119
  # @param files — project `.rb` paths to scan for call sites.
121
120
  # @param target_ruby — Prism parse target.
122
121
  # @param max_rounds — the WD5 fixpoint cap (1 = single-level).
123
- # @return => Hash{Symbol => Rigor::Type}}] frozen.
122
+ # @return the inferred parameter table, frozen.
124
123
  def self.collect(files:, environment:, target_ruby: nil, max_rounds: DEFAULT_ROUNDS, workers: 0)
125
124
  new(files: files, environment: environment, target_ruby: target_ruby,
126
125
  max_rounds: max_rounds, workers: workers).collect
@@ -13,7 +13,7 @@ module Rigor
13
13
  #
14
14
  # ADR-17 slice 2 gave `pre_eval:` a project-wide **method** registry ({ProjectPatchedMethods}). A
15
15
  # constant declared in the same file stayed invisible across the file boundary: `Scope#in_source_constants`
16
- # is per-file and is deliberately not part of `Runner#project_scope_seed_tables`, so only class-shaped
16
+ # was per-file and kept out of `Runner#project_scope_seed_tables`, so only class-shaped
17
17
  # constants (a `class` / `module` declaration, or one of the four meta-new forms `ScopeIndexer`'s
18
18
  # `record_class_new_constant_decl` promotes) ever crossed. `TIMEOUT = 30` in a listed file read as
19
19
  # `Dynamic[top]` everywhere else.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "external_ancestor_resolution"
5
+ require_relative "../run_scoped_slots"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -19,7 +20,7 @@ module Rigor
19
20
  NO_TARGETS = [].freeze
20
21
  private_constant :NO_TARGETS
21
22
 
22
- MEMO_KEY = :__rigor_project_method_ownership_memo__
23
+ MEMO_KEY = RunScopedSlots.register(:__rigor_project_method_ownership_memo__)
23
24
  private_constant :MEMO_KEY
24
25
 
25
26
  module_function
@@ -72,13 +73,15 @@ module Rigor
72
73
  # `Scope` merges each file's discovery with the project pre-pass, so the slot turns over per file, and
73
74
  # an ADR-46 dependency the first lookup recorded belongs to the file every later hit serves. Pool
74
75
  # workers are separate processes, and a Ractor has its own `Thread.current`. The `:ancestry` bucket
75
- # belongs to {ClosureEscapeAnalyzer}'s ancestry step.
76
+ # belongs to {ClosureEscapeAnalyzer}'s ancestry step. The `:block_runs` bucket belongs to
77
+ # {BlockRunCount}.
76
78
  def memo(scope)
77
79
  discovery = scope.discovery
78
80
  environment = scope.environment
79
81
  slot = Thread.current[MEMO_KEY]
80
82
  unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
81
- slot = [discovery, environment, { instance: {}, singleton: {}, ancestry: {} }]
83
+ buckets = { instance: {}, singleton: {}, ancestry: {}, block_runs: {}.compare_by_identity }
84
+ slot = [discovery, environment, buckets]
82
85
  Thread.current[MEMO_KEY] = slot
83
86
  end
84
87
  slot[2]