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
@@ -3,6 +3,7 @@
3
3
  require_relative "../type"
4
4
  require_relative "range_constant"
5
5
  require_relative "content_join"
6
+ require_relative "../typing_state"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -75,41 +76,70 @@ module Rigor
75
76
  }.freeze
76
77
  private_constant :TYPE_HANDLERS
77
78
 
78
- UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
79
- private_constant :UNREAD_HASH_REFUSED_KEY
80
-
81
- # Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
82
- # `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
83
- # of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
84
- # reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
85
- # `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
86
- # source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
87
- def maybe_rests_on_unread_hash?(param, arg)
88
- return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
89
- return false unless contains_type?(arg) { |type| unread_hash?(type) }
79
+ # ADR-129 WD1 — a Frame: {#refusing_unread} installs the kind of unread shape that answers `no` for one block
80
+ # and restores the previous one on the way out. It changes answers (`unread_shape_maybe` reads it), and the
81
+ # installed value is a Symbol (`:all`, `:array`) or nil.
82
+ UNREAD_SHAPE_REFUSED = TypingState.frame(
83
+ :acceptance_unread_shape_refused,
84
+ reason: "installed around a block by refusing_unread; read by unread_shape_maybe, which answers no for " \
85
+ "the refused kind of unread shape instead of maybe",
86
+ witness: "spec/fixtures/typing_state_witnesses/acceptance_unread_shape_refused.rb"
87
+ )
88
+ private_constant :UNREAD_SHAPE_REFUSED
89
+
90
+ # Whether a `maybe` for `param` against `arg` rests on a shape that could not read its argument: a record
91
+ # against a `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`, or a tuple against an
92
+ # `Array` of unknown arity ({#array_carrier?}). The shape cannot prove the key set or the length, and its
93
+ # `maybe` climbs through whatever holds it. `OverloadSelector`'s strict pass reads that `maybe` as no
94
+ # evidence for the overload, so this re-asks with each such shape answering `no` and reports whether the
95
+ # pair then fails. A `maybe` from anywhere else (a subclass only the Ruby source declares, a splat under a
96
+ # parameter that is no shape) survives the re-ask and still counts.
97
+ def maybe_rests_on_unread_shape?(param, arg)
98
+ return false unless rests_on_unread_hash?(param, arg) || rests_on_unread_array?(param, arg)
99
+
100
+ refusing_unread(:all) { accepts(param, arg, mode: :gradual).no? }
101
+ end
90
102
 
91
- refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
103
+ # Whether any of `types` holds an `Array` a tuple cannot read ({#array_carrier?}), so a match may rest on
104
+ # the tuple's `maybe`. `OverloadSelector`'s gradual pass asks before re-matching under
105
+ # {#refusing_unread_arrays}.
106
+ def unread_array_in?(types)
107
+ types.any? { |type| contains_type?(type) { |inner| array_carrier?(inner) } }
92
108
  end
93
109
 
110
+ # Runs the block with each tuple answering `no`, not `maybe`, for an `Array` of unknown arity. A record's
111
+ # `maybe` for an unread hash is untouched.
112
+ def refusing_unread_arrays(&) = refusing_unread(:array, &)
113
+
94
114
  # rubocop:disable-next Metrics/ClassLength
95
115
  class << self
96
116
  private
97
117
 
98
- # The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
99
- # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
100
- # through each carrier's `Type#accepts`. It is set only for the duration of the block.
101
- def refusing_unread_hashes
102
- previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
103
- Thread.current[UNREAD_HASH_REFUSED_KEY] = true
104
- yield
105
- ensure
106
- Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
118
+ def rests_on_unread_hash?(param, arg)
119
+ contains_type?(param) { |type| type.is_a?(Type::HashShape) } &&
120
+ contains_type?(arg) { |type| unread_hash?(type) }
107
121
  end
108
122
 
109
- # The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
110
- # {#refusing_unread_hashes}.
111
- def unread_hash_maybe(mode, reason)
112
- return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
123
+ def rests_on_unread_array?(param, arg)
124
+ contains_type?(param) { |type| type.is_a?(Type::Tuple) } &&
125
+ contains_type?(arg) { |type| array_carrier?(type) }
126
+ end
127
+
128
+ # The flag lives on the thread, not on a parameter, because the shape may sit any number of `accepts`
129
+ # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
130
+ # through each carrier's `Type#accepts`. It is set only for the duration of the block. `kind` is `:all`
131
+ # (records and tuples) or `:array` (tuples only).
132
+ def refusing_unread(kind, &)
133
+ TypingState.current.with(UNREAD_SHAPE_REFUSED, kind, &)
134
+ end
135
+
136
+ # The `maybe` a shape of `kind` (`:hash` or `:array`) answers for an argument it cannot read, or `no`
137
+ # inside {#refusing_unread} for that kind.
138
+ def unread_shape_maybe(mode, reason, kind)
139
+ refused = TypingState.current[UNREAD_SHAPE_REFUSED]
140
+ if refused == :all || refused == kind
141
+ return Type::AcceptsResult.no(mode: mode, reasons: "#{reason} (refused as evidence)")
142
+ end
113
143
 
114
144
  Type::AcceptsResult.maybe(mode: mode, reasons: reason)
115
145
  end
@@ -402,6 +432,19 @@ module Rigor
402
432
  CLASS_OBJECT_NOMINALS = %w[Module Class Object Kernel BasicObject].freeze
403
433
  private_constant :CLASS_OBJECT_NOMINALS
404
434
 
435
+ # Issue #2855 — whether `type` is an unparameterised `Class` or `Module` nominal: a class object whose
436
+ # identity inference lost (`const_get` kept by `is_a?(Class)`, an `is_a?(Module)` guard). `Acceptance` still
437
+ # answers `no` for it against a `Singleton` (it is a supertype, not a subtype); `call.argument-type-mismatch`
438
+ # reads it to withhold that verdict. `Object`, `BasicObject` and `Kernel` are left out: they mean "any value",
439
+ # and `Object.new` against `singleton(X)` is a real mismatch.
440
+ CLASS_OR_MODULE_NOMINALS = %w[Class Module].freeze
441
+ private_constant :CLASS_OR_MODULE_NOMINALS
442
+
443
+ def class_or_module_nominal?(type)
444
+ type.is_a?(Type::Nominal) && type.type_args.empty? && CLASS_OR_MODULE_NOMINALS.include?(type.class_name)
445
+ end
446
+ public :class_or_module_nominal?
447
+
405
448
  def accepts_nominal_from_singleton(self_type, other_type, mode)
406
449
  if CLASS_OBJECT_NOMINALS.include?(self_type.class_name)
407
450
  return Type::AcceptsResult.yes(
@@ -469,9 +512,10 @@ module Rigor
469
512
  end
470
513
 
471
514
  # Returns `other_type` rewritten so its type_args have the same arity as `self_type.type_args`, or
472
- # `nil` if no projection is known. Today only the Hash → Enumerable projection is hand-rolled; a
473
- # general RBS-driven implementation that consults `definition.ancestors[i].args` for arbitrary
474
- # subclass / module-include relations is the principled follow-up.
515
+ # `nil` if no projection is known. Today the Hash → Enumerable and ArithmeticSequence → Enumerator
516
+ # projections are hand-rolled; a general RBS-driven implementation that consults
517
+ # `definition.ancestors[i].args` for arbitrary subclass / module-include relations is the principled
518
+ # follow-up.
475
519
  def project_to_target_arity(self_type, other_type)
476
520
  return nil if self_type.type_args.size == other_type.type_args.size
477
521
  return nil if self_type.type_args.empty? || other_type.type_args.empty?
@@ -824,11 +868,14 @@ module Rigor
824
868
  # carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
825
869
  # another `Difference` with the same removed value (already exhibits the disjointness), an
826
870
  # `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
827
- # removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
828
- # last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
829
- # `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
830
- # rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
831
- # Intersection argument never reaches this rule whole: `accepts` splits it into members first.
871
+ # removed `Tuple`, a `HashShape` with a required key a closed removed `HashShape` cannot hold, or a
872
+ # `Refined` whose predicate recogniser rejects the removed `Constant`. The range, tuple and shape
873
+ # arms cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
874
+ # `non-empty-hash[K, V]`; the `Refined` arm is what lets `non-empty-string` accept
875
+ # `decimal-int-string`, which can never hold `""`. Any other carrier — a Nominal above all — is no
876
+ # proof and the difference rejects it under gradual mode, even where its values happen to exclude
877
+ # `removed`. A Union or Intersection argument never reaches this rule whole: `accepts` splits it
878
+ # into members first.
832
879
  def accepts_difference(self_type, other_type, mode)
833
880
  base_result = accepts(self_type.base, other_type, mode: mode)
834
881
  return base_result if base_result.no?
@@ -858,16 +905,30 @@ module Rigor
858
905
  # `removed`, the meet is too.
859
906
  other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
860
907
  when Type::IntegerRange
861
- removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
908
+ integer_range_excludes_removed?(other_type, removed)
862
909
  when Type::Tuple
863
910
  # A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
864
911
  # zero-arity witness `non-empty-array[T]` removes included.
865
912
  removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
866
913
  when Type::HashShape
867
914
  hash_shape_excludes_removed?(other_type, removed)
915
+ when Type::Refined
916
+ refined_excludes_removed?(other_type, removed)
868
917
  end
869
918
  end
870
919
 
920
+ def integer_range_excludes_removed?(range, removed)
921
+ removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !range.covers?(removed.value)
922
+ end
923
+
924
+ # A predicate refinement excludes the removed value when its recogniser rejects the removed
925
+ # constant outright — `decimal-int-string` can never hold `""`. `matches?` answers nil for a
926
+ # predicate the registry does not know, and nil is no proof, so the comparison is `== false`
927
+ # rather than `!`.
928
+ def refined_excludes_removed?(refined, removed)
929
+ removed.is_a?(Type::Constant) && refined.matches?(removed.value) == false
930
+ end
931
+
871
932
  # Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
872
933
  # key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
873
934
  # the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
@@ -881,8 +942,8 @@ module Rigor
881
942
  # `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
882
943
  # provably contained in the predicate's value set. The base check is delegated to
883
944
  # `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
884
- # their normal answer (the inner `accepts_nominal` does not register `Refined` / `Difference` as
885
- # direct other-shapes — projecting to the base is what makes the comparison meaningful).
945
+ # their normal answer (`accepts_nominal` projects a `Refined` / `Difference` other onto its base,
946
+ # which is what makes the comparison meaningful).
886
947
  #
887
948
  # Provability rules in gradual mode (the conservative analogue of `accepts_difference`):
888
949
  #
@@ -1003,9 +1064,12 @@ module Rigor
1003
1064
 
1004
1065
  # Tuple[A1..An] accepts:
1005
1066
  # - Tuple[B1..Bn] when arities match and each Ai accepts Bi (covariant per-position).
1006
- # - Anything else: no (we cannot prove the arity from a generic nominal alone).
1067
+ # - An `Array[E]` (see {#array_element_verdict}): maybe, or no on an element contradiction.
1068
+ # - Anything else: no.
1007
1069
  def accepts_tuple(self_type, other_type, mode)
1008
1070
  unless other_type.is_a?(Type::Tuple)
1071
+ return array_element_verdict(self_type, other_type, mode) if array_carrier?(other_type)
1072
+
1009
1073
  return Type::AcceptsResult.no(
1010
1074
  mode: mode,
1011
1075
  reasons: "Tuple does not accept #{other_type.class}"
@@ -1025,6 +1089,38 @@ module Rigor
1025
1089
  combine_arg_results(per_element, mode)
1026
1090
  end
1027
1091
 
1092
+ # An `Array` nominal, directly or as the base of a `Difference` (`non-empty-array[E]`). Its arity is
1093
+ # unknown, so a tuple cannot reject it on length: `URI.encode_www_form` takes
1094
+ # `Enumerable[[_ToS, _ToS]]` and is handed the `Array[Array[String]]` that `URI.decode_www_form`
1095
+ # answers. An `Array` subclass is not read, as {#gradual_hash?} does not read a `Hash` subclass.
1096
+ def array_carrier?(type)
1097
+ nominal = type.is_a?(Type::Difference) ? type.base : type
1098
+ nominal.is_a?(Type::Nominal) && nominal.class_name == "Array"
1099
+ end
1100
+
1101
+ # `maybe`, unless the element type rules the tuple out at any length: some position accepts no member
1102
+ # of `E` (`[Integer, Integer]` never takes an `Array[String]`). A raw `Array` stays `maybe`, and so does
1103
+ # an element with a gradual arm, since no position rejects a `Dynamic` member. The accepted cost (ADR-5)
1104
+ # is that a wrong-length `Array[E]` passed or returned where a tuple is declared goes unreported.
1105
+ def array_element_verdict(self_type, other_type, mode)
1106
+ nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
1107
+ element = nominal.type_args.first
1108
+ unless element.nil?
1109
+ members = ContentJoin.union_members(element)
1110
+ refused = self_type.elements.find do |formal|
1111
+ members.all? { |member| accepts(formal, member, mode: mode).no? }
1112
+ end
1113
+ if refused
1114
+ return Type::AcceptsResult.no(
1115
+ mode: mode,
1116
+ reasons: "tuple member #{refused.describe(:short)} rejects #{element.describe(:short)}"
1117
+ )
1118
+ end
1119
+ end
1120
+
1121
+ unread_shape_maybe(mode, "tuple arity unknown for #{other_type.describe(:short)}", :array)
1122
+ end
1123
+
1028
1124
  # HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
1029
1125
  # the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
1030
1126
  # when present, their values are checked. A closed self rejects known extra keys, and answers at most
@@ -1056,7 +1152,7 @@ module Rigor
1056
1152
  result = combine_arg_results(per_entry, mode)
1057
1153
  return result unless result.yes? && self_type.closed? && other_type.open?
1058
1154
 
1059
- unread_hash_maybe(mode, "HashShape closed target cannot check open source")
1155
+ unread_shape_maybe(mode, "HashShape closed target cannot check open source", :hash)
1060
1156
  end
1061
1157
 
1062
1158
  # A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
@@ -1085,7 +1181,7 @@ module Rigor
1085
1181
  refused = refused_value_keys(self_type, value_type, mode)
1086
1182
  return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
1087
1183
 
1088
- unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
1184
+ unread_shape_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}", :hash)
1089
1185
  end
1090
1186
 
1091
1187
  def unholdable_keys(self_type, key_type, mode)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_children"
6
+ require_relative "../closure_escape_analyzer"
7
+ require_relative "../remover_holder_scan"
8
+
9
+ module Rigor
10
+ module Inference
11
+ module AliasMutationFloor
12
+ # Issue #2335 — the *foreign* locals of a frame body ({AliasMutationFloor}): those whose object may have come
13
+ # from elsewhere, so they may be another local's object. A method parameter, a block or lambda parameter (but one
14
+ # of a catalogued iterator over literals that read nothing, whose parameters are those literals' own objects), a
15
+ # local any target node binds, and a local written anything but a new object.
16
+ module ForeignLocals
17
+ FRESH_VALUES = [
18
+ Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode,
19
+ Prism::InterpolatedXStringNode, Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
20
+ Prism::SymbolNode, Prism::InterpolatedSymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
21
+ Prism::RangeNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
22
+ ].freeze
23
+ # `+"x"`, `"x".dup`, `String.new`: a new String.
24
+ FRESH_STRING_CALLS = %i[+@ dup clone].to_set.freeze
25
+ LITERAL_PARTS = [Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode,
26
+ *FRESH_VALUES].freeze
27
+ private_constant :FRESH_VALUES, :FRESH_STRING_CALLS, :LITERAL_PARTS
28
+
29
+ module_function
30
+
31
+ # `[foreign, entry]`: the foreign locals, and the method parameters the body never rebinds, each frozen.
32
+ def collect(body, parameters)
33
+ foreign = Set.new
34
+ written = Set.new
35
+ collect_foreign(body, foreign, written)
36
+ finish(parameters, foreign, written)
37
+ end
38
+
39
+ # `[foreign, entry]` from the body's own `foreign` and `written` sets and the method's `parameters`.
40
+ def finish(parameters, foreign, written)
41
+ entry = Set.new
42
+ collect_parameters(parameters, entry)
43
+ [foreign.merge(entry).freeze, (entry - written).freeze]
44
+ end
45
+
46
+ def collect_parameters(node, names)
47
+ return if node.nil?
48
+
49
+ names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
50
+ node.rigor_each_child { |child| collect_parameters(child, names) }
51
+ end
52
+
53
+ # `foreign` gains every block or lambda parameter and every local a write binds to an object from elsewhere (an
54
+ # operator write's value is a call result); `written` every local any write or target binds. A nested def is
55
+ # not entered.
56
+ def collect_foreign(node, foreign, written, literal_blocks = Set.new.compare_by_identity)
57
+ return if node.is_a?(Prism::DefNode)
58
+
59
+ visit(node, foreign, written, literal_blocks)
60
+ node.rigor_each_child { |child| collect_foreign(child, foreign, written, literal_blocks) }
61
+ end
62
+
63
+ # {.collect_foreign}'s step for one node, which the site collector's walk also takes
64
+ # ({AliasMutationFloor::SiteCollector}).
65
+ def visit(node, foreign, written, literal_blocks)
66
+ case node
67
+ when Prism::CallNode then literal_blocks << node.block if literal_iterator_block?(node)
68
+ when Prism::BlockNode, Prism::LambdaNode
69
+ collect_parameters(node.parameters, foreign) unless literal_blocks.include?(node)
70
+ when Prism::LocalVariableTargetNode, Prism::LocalVariableOperatorWriteNode
71
+ foreign << written_name(node, written)
72
+ when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
73
+ name = written_name(node, written)
74
+ foreign << name unless fresh_value?(node.value)
75
+ end
76
+ end
77
+
78
+ # A catalogued iterator over literals that read nothing (`[[1], [2]].each { |x| }`,
79
+ # `[1].each_with_object({}) { |x, h| }`): its block's parameters are those literals' own new objects. Any other
80
+ # block's parameters may be any object.
81
+ def literal_iterator_block?(call_node)
82
+ return false unless call_node.block.is_a?(Prism::BlockNode)
83
+ return false unless ClosureEscapeAnalyzer.iterator_name?(call_node.name)
84
+
85
+ [call_node.receiver, *call_node.arguments&.arguments].all? do |operand|
86
+ !operand.nil? && literal_only?(operand)
87
+ end
88
+ end
89
+
90
+ def literal_only?(node)
91
+ return false unless LITERAL_PARTS.any? { |klass| node.is_a?(klass) }
92
+
93
+ node.rigor_each_child { |child| return false unless literal_only?(child) }
94
+ true
95
+ end
96
+
97
+ def written_name(node, written)
98
+ written << node.name
99
+ node.name
100
+ end
101
+
102
+ # A value that is a new object, so the local it binds holds no other holder's object.
103
+ def fresh_value?(value)
104
+ value = RemoverHolderScan.unparenthesised(value)
105
+ return true if FRESH_VALUES.any? { |klass| value.is_a?(klass) }
106
+ return false unless value.is_a?(Prism::CallNode)
107
+ return true if RemoverHolderScan.fresh_value?(value)
108
+
109
+ receiver = value.receiver
110
+ (FRESH_STRING_CALLS.include?(value.name) &&
111
+ (receiver.is_a?(Prism::StringNode) || receiver.is_a?(Prism::InterpolatedStringNode))) ||
112
+ (value.name == :new && receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :String)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../typing_state"
4
+ require_relative "../../memo_audit"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module AliasMutationFloor
9
+ # ADR-129 WD1 — the floor's four run-scoped Memos, each a pure function of the key it holds, and their accessors.
10
+ # Each accessor's block computes the value: on a miss it is stored, on a hit {MemoAudit.check} compares it with
11
+ # the stored one under `RIGOR_MEMO_AUDIT=1`, and while an audit recomputation runs ({MemoAudit.suspended?}) it
12
+ # answers without reading or writing the memo.
13
+ module Memos
14
+ FACTS = TypingState.memo(
15
+ :alias_mutation_floor_facts,
16
+ run_scoped: true, audit: "alias floor facts",
17
+ reason: "keyed by the body node; the facts are a pure function of the body and its parameters"
18
+ )
19
+ IVAR = TypingState.memo(
20
+ :alias_mutation_floor_ivar_mutators,
21
+ run_scoped: true, audit: "alias floor ivar mutator",
22
+ reason: "keyed by def node; whether its body mutates an ivar is a pure function of the body"
23
+ )
24
+ FLOOR = TypingState.memo(
25
+ :alias_mutation_floor_callee_floors,
26
+ run_scoped: true, audit: "alias floor callee floor",
27
+ reason: "keyed by the literal shape; the callee block floor is a pure function of it"
28
+ )
29
+ STORE = TypingState.memo(
30
+ :alias_mutation_floor_unknown_stores,
31
+ run_scoped: true, audit: "alias floor unknown store",
32
+ reason: "keyed by call node, removed-keys mode and type, every input of the widened store"
33
+ )
34
+ private_constant :FACTS, :IVAR, :FLOOR, :STORE
35
+
36
+ module_function
37
+
38
+ # The facts of `body`. A hit is audited without the facts' per-read memos, which fill as reads are answered.
39
+ def facts(body, &)
40
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
41
+
42
+ table = TypingState.current.weak_key_map(FACTS)
43
+ facts = table[body]
44
+ return table[body] = MemoAudit.key_audit("alias floor facts", &) if facts.nil?
45
+
46
+ MemoAudit.check("alias floor facts", audited(facts), body) { audited(yield) } if MemoAudit.enabled?
47
+ facts
48
+ end
49
+
50
+ def ivar_mutator(def_node, &)
51
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
52
+
53
+ table = TypingState.current.weak_key_map(IVAR)
54
+ cached = table[def_node]
55
+ return table[def_node] = MemoAudit.key_audit("alias floor ivar mutator", &) if cached.nil?
56
+
57
+ MemoAudit.check("alias floor ivar mutator", cached, def_node, &)
58
+ end
59
+
60
+ def callee_floor(type, &)
61
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
62
+
63
+ state = TypingState.current
64
+ table = (state[FLOOR] ||= {})
65
+ return table[type] = MemoAudit.key_audit("alias floor callee floor", &) unless table.key?(type)
66
+
67
+ MemoAudit.check("alias floor callee floor", table[type], type, &)
68
+ end
69
+
70
+ def unknown_store(call_node, removed_keys, type, &)
71
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
72
+
73
+ state = TypingState.current
74
+ table = (state[STORE] ||= {}.compare_by_identity)
75
+ per_mode = ((table[call_node] ||= {})[removed_keys] ||= {})
76
+ return per_mode[type] = MemoAudit.key_audit("alias floor unknown store", &) unless per_mode.key?(type)
77
+
78
+ MemoAudit.check("alias floor unknown store", per_mode[type], call_node, type, &)
79
+ end
80
+
81
+ def audited(facts) = facts.with(memo: nil, named_memo: nil)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../receiver_alias"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2335 — which locals a write through a receiver may reach ({AliasMutationFloor}), read off a frame body's
10
+ # {AliasMutationFloor::Facts}.
11
+ module AliasMutationFloor
12
+ module_function
13
+
14
+ # The locals aliased by the time `call_node`'s message runs, as `{ name => depth }`: those a site has handed on,
15
+ # and those linked to one ({.reach}). A call inside a closure that may run later sees every site.
16
+ def aliased_at(facts, call_node)
17
+ return ALL if facts.held.equal?(ALL)
18
+
19
+ offset = offset_of(facts, call_node)
20
+ seeds = {}
21
+ facts.held.each { |name, key| seeds[name] = FULL if key <= offset }
22
+ facts.held_part.each { |name, key| seeds[name] ||= PART if key <= offset }
23
+ reach(seeds, facts.links, offset)
24
+ end
25
+
26
+ def offset_of(facts, call_node)
27
+ offset = (call_node.message_loc || call_node.location).start_offset
28
+ facts.deferred.any? { |from, to| offset >= from && offset < to } ? Float::INFINITY : offset
29
+ end
30
+
31
+ # `seeds` closed over every link whose site may have run by `offset`. A depth says how far inside a local's object
32
+ # the mutated object may sit: 0 ({FULL}) is the object itself, 1 ({PART}) one of its elements, and so on. A copy
33
+ # keeps the depth; across an element link (`x = a[i]`, `x = a[i][j]`) the parent sits as many levels further out
34
+ # than the child as the read goes, and the child as many further in, so a mutation of the parent object itself
35
+ # (depth 0) does not reach the child.
36
+ # The least depth wins.
37
+ def reach(seeds, links, offset)
38
+ changed = !seeds.empty?
39
+ while changed
40
+ changed = false
41
+ links.each do |child, parent, key, levels|
42
+ next if key > offset
43
+
44
+ changed = true if spread(seeds, child, parent, levels) | spread(seeds, parent, child, -levels)
45
+ end
46
+ end
47
+ seeds
48
+ end
49
+
50
+ DEPTH_CAP = 8
51
+ private_constant :DEPTH_CAP
52
+
53
+ # Spreads the depth `from` reached across one link to `to`, `step` levels further out; answers whether `to`
54
+ # changed.
55
+ def spread(seeds, from, to, step) # rubocop:disable Naming/PredicateMethod
56
+ depth = seeds[from]
57
+ return false if depth.nil?
58
+
59
+ depth += step
60
+ return false if depth.negative? || depth > DEPTH_CAP
61
+ return false if seeds[to] && seeds[to] <= depth
62
+
63
+ seeds[to] = depth
64
+ true
65
+ end
66
+
67
+ # The locals a write through `receiver` may reach, as {.aliased_at} answers them: through each local it may
68
+ # evaluate to ({ReceiverAlias.candidates}: `(c ? a : b)`), through an element read's root ({.element_targets}), or
69
+ # as through any other receiver.
70
+ def receiver_targets(receiver, facts, call_node)
71
+ reads = ReceiverAlias.candidates(receiver)
72
+ aliased = aliased_at(facts, call_node)
73
+ return element_targets(receiver, facts, aliased, call_node) if reads.empty?
74
+ return targets(nil, facts, aliased, call_node) unless reads.all?(Prism::LocalVariableReadNode)
75
+
76
+ reads.map { |read| targets(read.name, facts, aliased, call_node) }.reduce { |acc, set| union(acc, set) }
77
+ end
78
+
79
+ # Through an element read rooted at a local (`c[0] << x`, `h[:k].fetch(0) << x`), the root's own links as far
80
+ # inside it as the read goes (`a = c[0]` is the mutated object itself), beside what any other receiver reaches.
81
+ def element_targets(receiver, facts, aliased, call_node)
82
+ others = targets(nil, facts, aliased, call_node)
83
+ root, depth = element_root(receiver)
84
+ return others if root.nil? || others.equal?(ALL)
85
+
86
+ union(others, reach({ root.name => depth }, facts.links, offset_of(facts, call_node)))
87
+ end
88
+
89
+ def union(left, right)
90
+ return ALL if left.equal?(ALL) || right.equal?(ALL)
91
+
92
+ merged = left.dup
93
+ right.each { |name, depth| merged[name] = [merged[name], depth].compact.min }
94
+ merged
95
+ end
96
+
97
+ # The locals a write through the local `name` (nil for any other receiver) may reach, as `{ name => depth }`:
98
+ #
99
+ # - the locals linked to it ({.reach}): a copy of it, or what it copied (`b = a; b << x`), or the local it is an
100
+ # element of (`x = a[i]`);
101
+ # - and every aliased local, through a receiver that may hold any object: an ivar, an index read, a call result,
102
+ # a block parameter, or a local some write binds to an object from elsewhere;
103
+ # - or only the aliased locals that are foreign, through a local only ever written a new object or a method
104
+ # parameter the body never rebinds, once the body has handed it on: either is an object no new-object local
105
+ # can be, and only a holder it reached can hand it to another local.
106
+ def targets(name, facts, aliased, call_node)
107
+ return ALL if aliased.equal?(ALL)
108
+ return aliased if name.nil? || NUMBERED_PARAMETER.match?(name)
109
+
110
+ group = reach({ name => FULL }, facts.links, offset_of(facts, call_node))
111
+ group.delete(name)
112
+ return union(aliased, group) if facts.foreign.include?(name) && !facts.entry.include?(name)
113
+ return group unless aliased.key?(name)
114
+
115
+ union(group, aliased.slice(*facts.foreign))
116
+ end
117
+ end
118
+ end
119
+ end