rigortype 0.4.6 → 0.4.8

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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -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,34 @@ 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
+ # An intersection counts when a member does: `non-empty-lowercase-string`'s witness half reads `tr!`'s
230
+ # replacement ({#widen_string_intersection}).
231
+ def shape_carrier?(type, synthesised: false)
164
232
  case type
165
233
  when Type::Tuple, Type::HashShape then true
166
- when Type::Constant then StringMutation.constant?(type)
234
+ when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
167
235
  # A re-openable nominal counts since issue #580: skipping it made the one-way door
168
236
  # self-fulfilling, since the arm never saw the arg types it grows from.
169
- when Type::Nominal then MutationRejoin.regrowable_carrier?(type)
237
+ # A precise `Array[E]` / `Set[E]` counts since issue #2361, which re-grows it from these arguments.
238
+ when Type::Nominal
239
+ MutationRejoin.regrowable_carrier?(type) || MutationRejoin.precise_array?(type) ||
240
+ SetMutation.regrowable?(type) || (synthesised && MutationRejoin.synthesised_carrier?(type))
170
241
  when Type::Difference then type.removes_empty_witness?
171
- when Type::Union then type.members.any? { |m| shape_carrier?(m) }
242
+ when Type::Union, Type::Intersection then type.members.any? { |m| shape_carrier?(m, synthesised:) }
172
243
  else false
173
244
  end
174
245
  end
175
246
 
176
247
  # Widens every variable `receiver` can evaluate to ({ReceiverAlias.mutated_reads}), against `method_name`'s
177
248
  # mutator table.
178
- def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES)
249
+ def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES, rewritten_element: nil,
250
+ removes_nil: false)
179
251
  return current_scope if receiver.nil?
180
252
 
181
253
  ReceiverAlias.mutated_reads(receiver).reduce(current_scope) do |acc, read|
182
- widen_alias_read(method_name, read, acc, arg_types: arg_types)
254
+ widen_alias_read(method_name, read, acc, arg_types:, rewritten_element:, removes_nil:)
183
255
  end
184
256
  end
185
257
 
@@ -196,9 +268,8 @@ module Rigor
196
268
  # always safe — it can only LOSE precision — so blindly propagating is sound regardless of
197
269
  # whether the block actually runs.
198
270
  #
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`.
271
+ # Recurses into every nested node, blocks included, so chained / nested forms (`arr << f(x); arr <<
272
+ # g(y)`, `arr.push(x) if cond`) are all caught.
202
273
  def widen_after_block(call_node:, outer_scope:)
203
274
  block = call_node.block
204
275
  return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
@@ -235,6 +306,13 @@ module Rigor
235
306
  receiver = call_node.receiver
236
307
  return scope if receiver.nil?
237
308
 
309
+ # Issue #2335 — a block parameter, an element read or any other receiver that may be an outer local's object.
310
+ if AliasMutationFloor.candidate?(call_node)
311
+ scope = AliasMutationFloor.floor_after_call(call_node, scope) { captured_element_root(receiver) }
312
+ end
313
+ widened = widen_outer_element_read(call_node, scope)
314
+ return widened if widened
315
+
238
316
  ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
239
317
  # A block-local read ({ReceiverAlias.block_local?}: a `depth == 0` local, or `it`) is not a
240
318
  # capture of the outer scope, so widening its name against the OUTER scope would hit an
@@ -245,10 +323,35 @@ module Rigor
245
323
  # after it re-adds the appended values' types onto the widened seed, so the seed's own value
246
324
  # pinning is still justified (`out = [0]; arr.each { out << x }` never rewrites slot 0). The
247
325
  # 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)
326
+ widen_alias_read(call_node.name, read, acc, values: :keep, unread_arguments: !call_node.arguments.nil?)
249
327
  end
250
328
  end
251
329
 
330
+ # The outer root of an element read in a block body, whose element {#widen_outer_element_read} widens. (A captured
331
+ # local the receiver names takes the capture path's widening and the block content join after it, which a floor
332
+ # here would not change.)
333
+ def captured_element_root(receiver)
334
+ root = ElementReadWidening.element_read_path(receiver)&.first
335
+ root && !ReceiverAlias.block_local?(root) ? [root.name] : nil
336
+ end
337
+
338
+ # Issue #2287 — a mutator on an element read rooted at a captured local (`[0].each { |i| a[i] << 2 }`,
339
+ # `loop { a[0] << 2; break }`) mutates an element the outer binding still carries, and names no variable
340
+ # {ReceiverAlias.candidates} could widen. The element at that path widens in the outer scope as the
341
+ # straight-line {ElementReadWidening.widen_element_read} widens it; a block-local root is skipped as a
342
+ # block-local read is. Answers the widened scope, or `nil` when the receiver is no such read or nothing widened.
343
+ def widen_outer_element_read(call_node, scope)
344
+ path = ElementReadWidening.element_read_path(call_node.receiver)
345
+ return nil if path.nil?
346
+
347
+ read, steps = path
348
+ return nil if ReceiverAlias.block_local?(read)
349
+
350
+ current = scope.local(read.name)
351
+ widened = current && ElementReadWidening.widen_through_path(current, steps, call_node.name, NO_ARG_TYPES)
352
+ widened && scope.with_mutated_local(read.name, widened)
353
+ end
354
+
252
355
  # The arms per kind of binding differ only in which pair of `Scope` accessors they use, so they are read out
253
356
  # of one table rather than written once per kind: a carrier the widening responds to must not be widened on
254
357
  # one kind of binding and left on another. `$g << x; $g == "k"` folded always-truthy on a `$g` Ruby holds
@@ -262,16 +365,37 @@ module Rigor
262
365
  Prism::GlobalVariableReadNode => %i[global with_global]
263
366
  })
264
367
 
265
- def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
368
+ # `unread_arguments` says the call passes arguments this seam did not type (the block-capture path): a Set,
369
+ # which has no block content join, takes one unknown store for it ({SetMutation.widen}).
370
+ def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false, # rubocop:disable Metrics/ParameterLists
371
+ rewritten_element: nil, removes_nil: false)
266
372
  getter, builder = ALIAS_ACCESSORS[read.class]
267
373
  return scope if getter.nil?
268
374
 
269
375
  name = ReceiverAlias.read_name(read)
270
376
  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?
377
+ synthesised = synthesised_binding?(scope, getter, name)
378
+ if synthesised && MutationRejoin.floors_grown_carrier?(scope, name, method_name)
379
+ return floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
380
+ end
273
381
 
274
- MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
382
+ widened = widen_for_mutator(current, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
383
+ synthesised:)
384
+ scope = MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter) if widened
385
+ return scope unless synthesised && MutationRejoin.content_adder?(method_name)
386
+
387
+ MutationRejoin.grew_unfloored?(current, widened) ? scope.with_construction_fact(name, :grown) : scope
388
+ end
389
+
390
+ # Issue #2555 — a remover or filter on a synthesised carrier a store has grown. Nothing narrows the carrier back,
391
+ # so the stored arm would outlive the values the call removes (`parts << nil; parts.compact!`); the carrier takes
392
+ # the `Dynamic[top]` floor it would have carried without the fact. The binding keeps its facts: the floored
393
+ # carrier is gradual, so the synthesised re-join no longer applies to it, and a later remover floors it again.
394
+ def floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
395
+ drop_nil = removes_nil || (method_name == :delete && MutationRejoin.hash_carrier?(current))
396
+ floored = MutationRejoin.floor_parameters(current, drop_nil:)
397
+ floored = widen_for_mutator(floored, method_name) || floored
398
+ MutationRejoin.rebind(scope, builder, name, floored, pre_state: current, kind: getter)
275
399
  end
276
400
 
277
401
  # Mutators that can land a NEW value in an EXISTING slot, falsifying that slot's value pinning
@@ -285,15 +409,32 @@ module Rigor
285
409
  # Returns the widened type for a binding whose receiver is about to be mutated by
286
410
  # `method_name`, or `nil` when no widening applies (binding is not a carrier the mutation
287
411
  # 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)
412
+ # nominal the call stores nothing into — see {MutationRejoin.widen_precise_array}).
413
+ # A `rewritten_element` answers first for the carriers {RewriteMutation.typed} types (issue #2427).
414
+ # `synthesised` says the binding carries the `:synthesised` construction fact (issue #2555,
415
+ # {Scope#construction_fact?}): a precise nominal or Set it holds re-grows without the `Dynamic[top]` floor
416
+ # ({MutationRejoin.widen_synthesised}, {SetMutation.widen}).
417
+ def widen_for_mutator(type, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
418
+ rewritten_element: nil, synthesised: false)
419
+ typed = rewritten_element && type && RewriteMutation.typed(type, method_name, rewritten_element)
420
+ return typed == type ? nil : typed if typed
421
+
422
+ widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
423
+ end
424
+
425
+ # {#widen_for_mutator} for a carrier no typed element rewrite answered.
426
+ def widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
427
+ synthesised: false)
290
428
  values = :keep unless VALUE_REWRITING_MUTATORS.include?(method_name)
291
429
  values = RewriteMutation.values_mode(type, method_name, values)
292
430
 
293
431
  return nil if type.nil?
432
+ if SetMutation.set?(type)
433
+ return SetMutation.widen(type, method_name, arg_types, unread_arguments:, synthesised:)
434
+ end
294
435
 
295
436
  case type
296
- when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:)
437
+ when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:, synthesised:)
297
438
  when Type::Tuple
298
439
  return nil unless ARRAY_MUTATORS.include?(method_name)
299
440
 
@@ -304,10 +445,9 @@ module Rigor
304
445
  join_added_pairs(widen_hash_shape(type, values: values), method_name, arg_types,
305
446
  ContentJoin.hash_shape_key_values(type))
306
447
  when Type::Constant then StringMutation.widen_constant(type, method_name)
307
- when Type::Difference
308
- widen_difference(type, method_name, arg_types: arg_types)
448
+ when Type::Difference, Type::Refined, Type::Intersection then widen_refinement(type, method_name, arg_types)
309
449
  when Type::Union
310
- widen_union(type, method_name, values: values, arg_types: arg_types)
450
+ widen_union(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
311
451
  end
312
452
  end
313
453
 
@@ -325,12 +465,14 @@ module Rigor
325
465
  #
326
466
  # `nil` when no member widened — a union with no carrier member, or one whose carriers are
327
467
  # 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)
468
+ def widen_union(union, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
469
+ rewritten_element: nil, synthesised: false)
329
470
  widened_any = false
330
471
  members = union.members.map do |member|
331
472
  next member unless mutation_carrier?(member, method_name)
332
473
 
333
- widened = widen_for_mutator(member, method_name, values: values, arg_types: arg_types)
474
+ widened = widen_for_mutator(member, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
475
+ synthesised:)
334
476
  next member if widened.nil?
335
477
 
336
478
  widened_any = true
@@ -348,7 +490,9 @@ module Rigor
348
490
  (ARRAY_MUTATORS.include?(method_name) && ContentJoin.array_residue(member).empty?) ||
349
491
  ((HASH_MUTATORS.include?(method_name) || HashLookupMutation::MUTATORS.include?(method_name)) &&
350
492
  ContentJoin.hash_residue(member).empty?) ||
351
- (StringMutation::MUTATORS.include?(method_name) && StringMutation.constant?(member))
493
+ (StringMutation::MUTATORS.include?(method_name) &&
494
+ (StringMutation.constant?(member) || StringMutation.refined?(member))) ||
495
+ (SetMutation::MUTATORS.include?(method_name) && SetMutation.carrier?(member))
352
496
  end
353
497
 
354
498
  # Joins the element evidence the mutator's own ARGUMENTS introduce into the already-widened
@@ -530,6 +674,32 @@ module Rigor
530
674
  end
531
675
  end
532
676
 
677
+ # An intersection of String refinements carrying a predicate one (`non-empty-lowercase-string`) widens member
678
+ # by member: the `Refined` member to `String` ({StringMutation.widen_refined}), a `non-empty-string` member on
679
+ # {#widen_difference}'s witness terms. A member that widened to the bare `String` is implied by the others and
680
+ # drops out, so `s.upcase!` reads `non-empty-string` rather than `non-empty-string & String`. `nil` when the
681
+ # intersection carries no String predicate refinement, or nothing widened.
682
+ def widen_string_intersection(intersection, method_name, arg_types)
683
+ return nil unless intersection.members.any? { |m| StringMutation.refined?(m) }
684
+
685
+ members = intersection.members.map { |m| widen_refinement(m, method_name, arg_types) || m }
686
+ base = Type::Combinator.nominal_of("String")
687
+ kept = members.reject { |m| m == base }
688
+ widened = kept.empty? ? base : Type::Combinator.intersection(*kept)
689
+ widened == intersection ? nil : widened
690
+ end
691
+
692
+ # The refinement carriers {#widen_carrier} hands on: the empty witness ({#widen_difference}), a String predicate
693
+ # ({StringMutation.widen_refined}) and an intersection of them ({#widen_string_intersection}). `nil` when nothing
694
+ # widened.
695
+ def widen_refinement(type, method_name, arg_types)
696
+ case type
697
+ when Type::Difference then widen_difference(type, method_name, arg_types:)
698
+ when Type::Refined then StringMutation.widen_refined(type, method_name)
699
+ when Type::Intersection then widen_string_intersection(type, method_name, arg_types)
700
+ end
701
+ end
702
+
533
703
  # `Tuple[A, B, C]` → `Nominal[Array, [union(A, B, C)]]`; under `values: :widen`, each element's
534
704
  # VALUE pinning widens to its class nominal (`1 | 2` → `Integer`): a slot-rewriting mutator
535
705
  # falsifies values along with the shape — `t = [1, 2]; t[0] += 5` holds `6` at slot 0, so a