rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -0,0 +1,815 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../source/node_walker"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2595 — the writes a def makes to its parameters that the callee floor's direct and block censuses
11
+ # (`StatementEvaluator#own_content_parameters`) do not see, read from the def's AST alone:
12
+ #
13
+ # - a shape mutator the content scans do not count (`h.merge!(x)`, `h.update(x)`, `a.pop`, `h.compact!`), from
14
+ # `MutationWidening::SHAPE_MUTATORS`, which the caller passes in as `mutators:`;
15
+ # - a mutation whose receiver may be the parameter or a part of it: the parameter read through any expression
16
+ # (`(h).merge!`, `h.itself.merge!`, `[h].first.merge!`, `(c ? {} : h).merge!`), or an alias. An alias is a
17
+ # local, ivar, class variable or global assigned from it (`other = h`, `x = y = h`, `a, b = h, 1`, `@x = h`,
18
+ # `@@x = h`, `$g = h`, `instance_variable_set(:@x, h)`), a block parameter of a call whose receiver or argument
19
+ # holds it (`h.tap { |x| }`, `[h].each { |x| }`, `each_with_object(h)`), or a call result on it
20
+ # (`v = h[:list]`) unless the call returns a new object (`fresh:`, `h.dup`, `h.merge(x)`). A literal, and a
21
+ # copier's result (`copiers:`, `h.values`, `h.slice(:a)`), only holds what it is built from (`t = [h]`): a
22
+ # write to it does not count, an element taken out does;
23
+ # - an opaque dispatch on such a value (`send`, `instance_eval`, `method`, ...) and a mutator symbol block
24
+ # (`[h].each(&:clear)`).
25
+ #
26
+ # Issue #2336 — and a hand-off the callee floor cannot resolve, which reads the handed value as written: an
27
+ # argument of `yield`, of `call` on any explicit receiver (a Method, a Proc, a lambda: `method(:poke).call(h)`,
28
+ # `->(x) { }.call(h)`, `blk.(h)`), of `instance_exec` / `class_exec` / `module_exec`, or of a `send` /
29
+ # `public_send` / `__send__` whose method name is no literal; an argument of any call inside a block that runs
30
+ # later as a method body (`define_method(:go) { poke(h) }`, {Tables#foreign_calls}), which the hand-off walk
31
+ # leaves out; and, when `returns:` is true, a value the def returns (its body's last expression, a `return`
32
+ # outside a lambda) that is the parameter itself, whose caller may write it (`r = ident(h); r[:k] = 1`).
33
+ #
34
+ # `seeds` names the parameters and how each holds the caller's argument: a positional or keyword parameter is it
35
+ # (`:same`); a rest or keyword-rest parameter holds the caller's arguments as elements (`:held`), so a write to the
36
+ # rest array itself does not count and a write to an element taken out of it does.
37
+ #
38
+ # The walk is flow-insensitive, so an alias made anywhere counts everywhere; a local is keyed by its name and the
39
+ # scope level it lives at, so two sibling blocks' locals of one name share a key. Both only ever count more. A
40
+ # nested def is not entered; a block and a lambda are. A write the content scans already count on the parameter
41
+ # read directly (a name in `handled:`, an index write on `h`) is left to them: their block channel keeps the element
42
+ # type. `rebinds` (`{ name => end offset }`, `StatementEvaluator#parameter_rebinds`) says where the def rebinds a
43
+ # parameter to a new object (`h = h.dup`): a node that starts after it no longer reaches the caller's argument,
44
+ # read as `StatementEvaluator#after_rebind?` reads it.
45
+ #
46
+ # {Census#aliases} answers the parameters a read may be (not a part of), so a hand-off of an alias (`poke(other)`)
47
+ # is followed as the parameter's own hand-off.
48
+ module CalleeWriteCensus
49
+ # `written` is the parameter names written; `alias_table` maps a key (`[local, level]`, `[ivar, :ivar]` or
50
+ # `[cvar_or_global, :global]`) to `{ status => Set[parameter] }`: the value under the key may be the parameter
51
+ # (status `:same`), a part of it (`:part`: `v = h[:list]`, `h.each { |k, v| }`), or hold it as an element
52
+ # (`:held`: `arr = [h]`, which a write to `arr` does not reach).
53
+ Census = Data.define(:written, :alias_table, :names)
54
+
55
+ # The name tables a scan reads, which the caller owns: `mutators` (the shape and content mutators), `handled`
56
+ # (the names the content scans count on the parameter read directly), `fresh` (calls whose result is a new
57
+ # object), `copiers` (calls whose result is a new container of the receiver's elements) and `foreign_calls`
58
+ # (calls whose block is a method body).
59
+ Tables = Data.define(:mutators, :handled, :fresh, :copiers, :foreign_calls)
60
+
61
+ NONE = [].freeze
62
+ EMPTY = Census.new(written: NONE, alias_table: {}.freeze, names: Set.new.freeze)
63
+ STATUSES = %i[same part held].freeze
64
+ STATUS_INDEX = { same: 0, part: 1, held: 2 }.freeze
65
+ # A step is how a value relates to the one it is built from, as the status it gives each of {STATUSES}.
66
+ ID = %i[same part held].freeze
67
+ WRAP = %i[held held held].freeze
68
+ UNWRAP = %i[part part same].freeze
69
+ # The mode of a mutator symbol block's record: every status counts, since the elements take the mutator.
70
+ ANY = :any
71
+ # Calls whose result is the receiver itself (or, for `to_a`, `to_h` and `to_s`, may be: each returns its receiver
72
+ # on one core class and a copy on another, read as the receiver), calls whose block parameter is the receiver,
73
+ # and calls whose result is the block's value.
74
+ SELF_RETURNERS = %i[tap itself freeze to_a to_h to_s].to_set.freeze
75
+ SELF_YIELDERS = %i[tap then yield_self].to_set.freeze
76
+ BLOCK_RETURNERS = %i[then yield_self].to_set.freeze
77
+ # Memo iterators whose memo parameter comes first (`inject(m) { |memo, e| }`); `each_with_object` puts it last.
78
+ MEMO_FIRST = %i[inject reduce].to_set.freeze
79
+ # Calls whose result is their memo argument's (`h.each_with_object({}) { ... }` is that `{}`).
80
+ MEMO_RETURNERS = %i[each_with_object inject reduce].to_set.freeze
81
+ # Iterators whose block parameter is a new array of the receiver's elements, and which return the receiver when
82
+ # given a block and a new enumerator otherwise.
83
+ SLICERS = %i[each_slice each_cons].to_set.freeze
84
+ # Calls that copy (a new container of the receiver's elements) only when given an argument (`first(2)`).
85
+ ARGUMENT_COPIERS = %i[first last min max].to_set.freeze
86
+ # Every step {ID}, {WRAP} and {UNWRAP} compose to, as `{ outer => { inner => outer after inner } }`, closed once
87
+ # at load: the steps are a handful of frozen tables, so the walk composes them without allocating.
88
+ COMPOSED = begin
89
+ steps = [ID, WRAP, UNWRAP]
90
+ table = {}
91
+ loop do
92
+ size = steps.size
93
+ steps.product(steps).each do |outer, inner|
94
+ composed = inner.map { |status| outer[STATUS_INDEX.fetch(status)] }
95
+ steps << composed.freeze unless steps.include?(composed)
96
+ (table[outer] ||= {})[inner] = steps[steps.index(composed)]
97
+ end
98
+ break if steps.size == size
99
+ end
100
+ table.each_value(&:freeze).freeze
101
+ end
102
+
103
+ # Calls on an alias that run code the walk cannot see against it.
104
+ OPAQUE = %i[send public_send __send__ instance_eval instance_exec instance_variable_set method
105
+ public_method].to_set.freeze
106
+ # The names {Census#written} reports an anonymous rest (`*`), keyword rest (`**`) or `...` under when one is
107
+ # handed to an opaque sink; no parameter can be named so.
108
+ ANONYMOUS_REST = :*
109
+ ANONYMOUS_KEYWORD_REST = :**
110
+ ANONYMOUS_FORWARDED = [ANONYMOUS_REST, ANONYMOUS_KEYWORD_REST].freeze
111
+ # Calls whose arguments the walk cannot follow, on an explicit receiver for `call` and on any for the rest.
112
+ OPAQUE_ARGUMENTS = %i[call instance_exec class_exec module_exec].to_set.freeze
113
+ # Dispatches whose target is their first argument: opaque to the walk when it is no literal name.
114
+ SENDS = %i[send public_send __send__].to_set.freeze
115
+ IVAR_ANY_KEY = %i[* ivar].freeze
116
+ PARAMETER_NODES = [Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
117
+ Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode,
118
+ Prism::KeywordRestParameterNode].freeze
119
+ private_constant :STATUSES, :STATUS_INDEX, :ID, :WRAP, :UNWRAP, :ANY, :SELF_RETURNERS, :SELF_YIELDERS,
120
+ :BLOCK_RETURNERS, :MEMO_FIRST, :MEMO_RETURNERS, :SLICERS, :ARGUMENT_COPIERS, :COMPOSED, :OPAQUE,
121
+ :OPAQUE_ARGUMENTS, :ANONYMOUS_FORWARDED, :SENDS, :IVAR_ANY_KEY, :PARAMETER_NODES
122
+
123
+ module_function
124
+
125
+ # The {Census} of `body` for the parameters `seeds` (`{ name => :same | :held }`), reading `tables` ({Tables}).
126
+ # `returns` says whether a returned parameter counts.
127
+ #
128
+ # `anonymous` says the def has an anonymous rest, keyword rest or `...`, which the walk reads even with no
129
+ # seed: handed to an opaque sink, it reports {ANONYMOUS_REST} / {ANONYMOUS_KEYWORD_REST} as written.
130
+ def scan(body, seeds, tables:, rebinds:, returns: false, anonymous: false)
131
+ return EMPTY if body.nil? || (seeds.empty? && !anonymous)
132
+
133
+ walk = Walk.new(tables, returns: returns)
134
+ walk.visit(body, 0)
135
+ walk.note_tail(body, 0) if returns
136
+ walk.census(seeds, rebinds)
137
+ end
138
+
139
+ # True when a `send` / `public_send` / `__send__` call names its target with a literal (`send(:poke, h)`,
140
+ # `send("poke", h)`), which the callee floor resolves as a call of that name.
141
+ def literal_send?(node)
142
+ target = node.arguments&.arguments&.first
143
+ target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode)
144
+ end
145
+
146
+ # The parameter names an argument `node` at walk depth `depth` may BE (not a part of, nor a container holding):
147
+ # through an alias in `census` (`other = h; poke(other)`), or through an expression over a parameter or alias
148
+ # (`poke(args.first)`, `poke(h.itself)`). A read of a parameter itself answers nothing here; the caller reads it.
149
+ def handed_aliases(census, seeds, node, depth, fresh:, copiers:)
150
+ if (name = read_name(node))
151
+ return NONE unless census.names.include?(name)
152
+
153
+ return lookup(census.alias_table, read_key(node, depth), :same)
154
+ end
155
+ return NONE unless node.is_a?(Prism::CallNode) || node.is_a?(Prism::ParenthesesNode)
156
+
157
+ tables = Tables.new(mutators: NONE, handled: NONE, fresh: fresh, copiers: copiers, foreign_calls: NONE)
158
+ found = Walk.new(tables).value_sources(node, depth)
159
+ found.nil? ? NONE : same_parameters(census, seeds, found)
160
+ end
161
+
162
+ # The parameters the flat `found` sources give a value as the value itself.
163
+ def same_parameters(census, seeds, found)
164
+ result = nil
165
+ each_record(found, 2) do |key, step|
166
+ STATUSES.each do |status|
167
+ next unless apply(step, status) == :same
168
+
169
+ parameters = lookup(census.alias_table, key, status)
170
+ (result ||= []).concat(parameters) unless parameters.empty?
171
+ end
172
+ seed = seed(key, seeds)
173
+ (result ||= []) << key.first if seed && apply(step, seed) == :same
174
+ end
175
+ result.nil? ? NONE : result.uniq
176
+ end
177
+
178
+ # The name part of {#read_key}, allocating nothing; nil for anything but a read.
179
+ def read_name(node)
180
+ case node
181
+ when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
182
+ Prism::GlobalVariableReadNode
183
+ node.name
184
+ when Prism::ItLocalVariableReadNode then :it
185
+ end
186
+ end
187
+
188
+ # The key a read node stands for at walk depth `depth`, or nil.
189
+ def read_key(node, depth)
190
+ case node
191
+ when Prism::LocalVariableReadNode then [node.name, depth - node.depth]
192
+ when Prism::ItLocalVariableReadNode then [:it, depth]
193
+ when Prism::InstanceVariableReadNode then [node.name, :ivar]
194
+ when Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode then [node.name, :global]
195
+ end
196
+ end
197
+
198
+ # The parameters `key` holds under `status`; an ivar also holds what a non-literal `instance_variable_set` stored.
199
+ def lookup(table, key, status)
200
+ found = table[key]&.[](status)
201
+ if key.last == :ivar
202
+ any = table[IVAR_ANY_KEY]&.[](status)
203
+ found = found.nil? ? any : found | any if any
204
+ end
205
+ found.nil? ? NONE : found.to_a
206
+ end
207
+
208
+ # The status a value reached through `step` has when its source has `status`: a literal wraps it (`[h]` holds
209
+ # `h`), a call or an element of it unwraps it (`[h].first` is `h`, `h[:list]` a part of `h`), a self-returner
210
+ # (`h.tap`) and anything else pass it on.
211
+ def apply(step, status) = step[STATUS_INDEX.fetch(status)]
212
+
213
+ # How the parameter `key` reads holds the caller's argument (`seeds`), or nil when `key` is no parameter read at
214
+ # method scope.
215
+ def seed(key, seeds) = key.last.equal?(0) ? seeds[key.first] : nil
216
+
217
+ # Yields each `width`-long record (at most four) of the flat `list` (nil: none) as separate arguments, allocating
218
+ # nothing; a block taking fewer arguments ignores the rest.
219
+ def each_record(list, width)
220
+ return if list.nil?
221
+
222
+ i = 0
223
+ while i < list.size
224
+ yield list[i], list[i + 1], list[i + 2], list[i + 3]
225
+ i += width
226
+ end
227
+ end
228
+
229
+ # True when `node` starts after the def rebinds the parameter `name` ({#scan}'s `rebinds`).
230
+ def rebound?(rebinds, name, node)
231
+ offset = rebinds[name]
232
+ !offset.nil? && node.location.start_offset >= offset
233
+ end
234
+
235
+ # The step `outer` after `inner` ({COMPOSED}).
236
+ def compose(outer, inner) = COMPOSED.fetch(outer).fetch(inner)
237
+
238
+ # True when a mutation under `mode` (a step, or {ANY}) reaches a value of `status`: the value itself or a part
239
+ # of it, not a container holding it, unless every status counts.
240
+ def reaches?(mode, status) = mode == ANY || apply(mode, status) != :held
241
+
242
+ # The value forms {Walk} reads a value's sources through: what a value may be built from, and how.
243
+ module ValueSources
244
+ private
245
+
246
+ # `acc` (nil until the first) with each `key, step` a value may be built from at `depth` appended, flat;
247
+ # `step` is how the value relates to the key ({CalleeWriteCensus.apply}).
248
+ def sources(node, depth, acc, step)
249
+ case node
250
+ when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
251
+ Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
252
+ (acc ||= []).push(CalleeWriteCensus.read_key(node, depth), step)
253
+ when Prism::ParenthesesNode, Prism::StatementsNode, Prism::OrNode, Prism::AndNode, Prism::ElseNode,
254
+ Prism::AssocNode, Prism::SplatNode, Prism::AssocSplatNode
255
+ node.rigor_each_child { |child| acc = sources(child, depth, acc, step) }
256
+ when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode
257
+ inner = CalleeWriteCensus.compose(step, WRAP)
258
+ node.rigor_each_child { |child| acc = sources(child, depth, acc, inner) }
259
+ when Prism::CallNode then acc = call_sources(node, depth, acc, step)
260
+ else acc = branch_sources(node, depth, acc, step)
261
+ end
262
+ acc
263
+ end
264
+
265
+ # The value forms that pick one of several branches, or pass on a write's value (`x = y = h`).
266
+ def branch_sources(node, depth, acc, step)
267
+ case node
268
+ when Prism::IfNode then sources(node.subsequent, depth, sources(node.statements, depth, acc, step), step)
269
+ when Prism::UnlessNode
270
+ sources(node.else_clause, depth, sources(node.statements, depth, acc, step), step)
271
+ when Prism::CaseNode, Prism::CaseMatchNode
272
+ node.conditions.each { |condition| acc = sources(condition.statements, depth, acc, step) }
273
+ sources(node.else_clause, depth, acc, step)
274
+ when Prism::BeginNode then begin_sources(node, depth, acc, step)
275
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
276
+ Prism::GlobalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
277
+ sources(node.value, depth, acc, step)
278
+ else acc
279
+ end
280
+ end
281
+
282
+ # A `begin` block's value: its statements, each `rescue` clause's, or its `else` clause's.
283
+ def begin_sources(node, depth, acc, step)
284
+ acc = sources(node.statements, depth, acc, step)
285
+ rescue_clause = node.rescue_clause
286
+ while rescue_clause
287
+ acc = sources(rescue_clause.statements, depth, acc, step)
288
+ rescue_clause = rescue_clause.subsequent
289
+ end
290
+ sources(node.else_clause, depth, acc, step)
291
+ end
292
+
293
+ def call_sources(node, depth, acc, step)
294
+ name = node.name
295
+ return acc if @fresh.include?(name)
296
+ # A copier builds a new container of the receiver's elements (`h.values`, `h.slice(:a)`, `to_h { }`): a write
297
+ # to it misses the receiver, an element taken out of it may be a part.
298
+ return memo_sources(node, depth, acc, step) if MEMO_RETURNERS.include?(name) && node.arguments
299
+ return sources(node.receiver, depth, acc, step) if SLICERS.include?(name) && node.block
300
+
301
+ return sources(node.receiver, depth, acc, CalleeWriteCensus.compose(step, WRAP)) if copier?(node)
302
+ return ivar_get_sources(node, acc, step) if name == :instance_variable_get && self_receiver?(node)
303
+
304
+ if BLOCK_RETURNERS.include?(name) && node.block.is_a?(Prism::BlockNode)
305
+ acc = sources(node.block.body, depth + 1, acc, step)
306
+ end
307
+ inner = SELF_RETURNERS.include?(name) ? step : CalleeWriteCensus.compose(step, UNWRAP)
308
+ sources(node.receiver, depth, acc, inner)
309
+ end
310
+
311
+ def copier?(node)
312
+ name = node.name
313
+ @copiers.include?(name) || SLICERS.include?(name) || (name == :to_h && node.block) ||
314
+ (ARGUMENT_COPIERS.include?(name) && node.arguments)
315
+ end
316
+
317
+ # A memo iterator's result: `each_with_object`'s is its memo argument; `inject(m)`'s is the block's last value,
318
+ # read as the argument or an element of the receiver.
319
+ def memo_sources(node, depth, acc, step)
320
+ node.arguments.arguments.each { |argument| acc = sources(argument, depth, acc, step) }
321
+ return acc if node.name == :each_with_object
322
+
323
+ sources(node.receiver, depth, acc, CalleeWriteCensus.compose(step, UNWRAP))
324
+ end
325
+
326
+ def ivar_get_sources(node, acc, step)
327
+ target = node.arguments&.arguments&.first
328
+ (acc || []).push(target.is_a?(Prism::SymbolNode) ? [target.unescaped.to_sym, :ivar] : IVAR_ANY_KEY, step)
329
+ end
330
+ end
331
+ private_constant :ValueSources
332
+
333
+ # How {Walk} binds a block's parameters to the values of the call that yields to it.
334
+ module BlockBindings
335
+ private
336
+
337
+ # A block parameter takes an element of the receiver (`[h].each { |x| }`, `h.each { |k, v| }`), the receiver
338
+ # itself under a self-yielder (`h.tap { |x| }`), or an argument itself (`each_with_object(h) { |_, m| }`). A
339
+ # slicer's block parameter (`each_slice`, `each_cons`) is a new array of the receiver's elements.
340
+ def note_block(node, depth)
341
+ params = block_parameters(node.block, depth + 1)
342
+ return if params.empty?
343
+
344
+ arguments = node.arguments&.arguments
345
+ memo = memo_index(node.name, node.block, params) if arguments && !arguments.empty?
346
+ found = sources(node.receiver, depth, nil, receiver_block_step(node.name, node.block))
347
+ return note_memo_block(node, depth, params, memo, found) if memo
348
+
349
+ arguments&.each { |argument| found = sources(argument, depth, found, ID) }
350
+ edge(found, node) { params } if found
351
+ end
352
+
353
+ # A memo iterator with a memo argument (`each_with_object(m)`, `inject(m)`, `reduce(m)`) binds its memo
354
+ # parameter (at `memo`) from the argument alone, and the element parameters from the receiver (`found`).
355
+ def note_memo_block(node, depth, params, memo, found)
356
+ elements = params.dup.tap { |list| list.delete_at(memo) }
357
+ edge(found, node) { elements } if found && !elements.empty?
358
+ held = nil
359
+ node.arguments.arguments.each { |argument| held = sources(argument, depth, held, ID) }
360
+ edge(held, node) { params[memo] } if held
361
+ end
362
+
363
+ # A slicer's block parameter holds a slice only when the block takes it whole (`|s|`, `it`, `_1` alone); a
364
+ # block that destructures it (`|a, b|`, `_2`) takes the receiver's elements.
365
+ def receiver_block_step(name, block)
366
+ if SELF_YIELDERS.include?(name) then ID
367
+ elsif SLICERS.include?(name) && whole_parameter?(block) then WRAP
368
+ else UNWRAP
369
+ end
370
+ end
371
+
372
+ # The index, among the `keys` a memo iterator `name`'s block binds, of its memo parameter, or nil. The memo is
373
+ # bound only when the block takes at least two top-level positional parameters (requireds and optionals) and
374
+ # the memo's is a plain required one (the last of `each_with_object`'s, the first of `inject`'s); `|x|`, `it`
375
+ # or `_1` alone is the element, and a destructured memo binds nothing.
376
+ def memo_index(name, block, keys)
377
+ positional = top_level_parameters(block)
378
+ return if positional.nil? || positional.size < 2
379
+
380
+ memo = name == :each_with_object ? positional.last : (positional.first if MEMO_FIRST.include?(name))
381
+ memo_name = parameter_name(memo)
382
+ memo_name && keys.index { |key| key.first == memo_name }
383
+ end
384
+
385
+ # The name a plain parameter binds: a required parameter's, or a numbered placeholder's (`:_2`); nil otherwise.
386
+ def parameter_name(param)
387
+ case param
388
+ when Symbol then param
389
+ when Prism::RequiredParameterNode then param.name
390
+ end
391
+ end
392
+
393
+ # The block's top-level positional parameters, requireds then optionals (`:_1`, `:_2` for numbered ones), or nil
394
+ # for `it` / none.
395
+ def top_level_parameters(block)
396
+ params = block.parameters
397
+ case params
398
+ when Prism::NumberedParametersNode then (1..params.maximum).map { |i| :"_#{i}" }
399
+ when Prism::BlockParametersNode
400
+ inner = params.parameters
401
+ inner.nil? ? nil : [*inner.requireds, *inner.optionals]
402
+ end
403
+ end
404
+
405
+ # True when the block takes its one yielded value whole: `it`, `_1` alone, or one plain required parameter.
406
+ def whole_parameter?(block)
407
+ params = block.parameters
408
+ case params
409
+ when Prism::ItParametersNode then true
410
+ when Prism::NumberedParametersNode then params.maximum == 1
411
+ when Prism::BlockParametersNode
412
+ inner = params.parameters
413
+ !inner.nil? && inner.requireds.size == 1 && inner.requireds.first.is_a?(Prism::RequiredParameterNode) &&
414
+ inner.optionals.empty? && inner.rest.nil? && inner.posts.empty? && inner.keywords.empty? &&
415
+ inner.keyword_rest.nil?
416
+ else false
417
+ end
418
+ end
419
+
420
+ def block_parameters(block, depth)
421
+ params = block.parameters
422
+ case params
423
+ when Prism::NumberedParametersNode then (1..params.maximum).map { |i| [:"_#{i}", depth] }
424
+ when Prism::ItParametersNode then [[:it, depth]]
425
+ when Prism::BlockParametersNode
426
+ found = []
427
+ Source::NodeWalker.each(params.parameters) do |param|
428
+ found << [param.name, depth] if PARAMETER_NODES.any? { |klass| param.is_a?(klass) } && param.name
429
+ end
430
+ found
431
+ else NONE
432
+ end
433
+ end
434
+ end
435
+ private_constant :BlockBindings
436
+
437
+ # Issue #2336 — how {Walk} reads the values a def returns ({CalleeWriteCensus.scan}'s `returns:`).
438
+ module Returns
439
+ # Records what the def's value may be: the last expression of each branch of `node` (a body, a branch, a
440
+ # `begin` clause), read as {ValueSources#sources} reads a value.
441
+ def note_tail(node, depth)
442
+ case node
443
+ when nil, Prism::ReturnNode then nil
444
+ when Prism::StatementsNode then note_tail(node.body.last, depth)
445
+ when Prism::ParenthesesNode then note_tail(node.body, depth)
446
+ when Prism::ElseNode then note_tail(node.statements, depth)
447
+ when Prism::IfNode, Prism::UnlessNode
448
+ note_tail(node.statements, depth)
449
+ note_tail(node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause, depth)
450
+ when Prism::CaseNode, Prism::CaseMatchNode
451
+ node.conditions.each { |condition| note_tail(condition.statements, depth) }
452
+ note_tail(node.else_clause, depth)
453
+ when Prism::BeginNode then note_begin_tail(node, depth)
454
+ else note_returned(node, depth)
455
+ end
456
+ end
457
+
458
+ # A `return` outside a lambda returns from the def, when the scan counts returns ({#note_tail} ran first).
459
+ def note_return(node, depth)
460
+ values = node.arguments&.arguments
461
+ note_returned(values.first, depth) if values&.size == 1 && @lambdas.zero? && @returns
462
+ end
463
+
464
+ def note_begin_tail(node, depth)
465
+ note_tail(node.statements, depth)
466
+ rescue_clause = node.rescue_clause
467
+ while rescue_clause
468
+ note_tail(rescue_clause.statements, depth)
469
+ rescue_clause = rescue_clause.subsequent
470
+ end
471
+ note_tail(node.else_clause, depth)
472
+ end
473
+
474
+ # Records the variable a value the def may return reads, `key, node` flat: a read itself, through a write's
475
+ # value (`@x = h`), either side of `||` / `&&` and a parenthesised body. A call's result is not read as the
476
+ # parameter, even one that returns its receiver (`h.tap { }`), so the census stays exact on the spellings it
477
+ # reads; an element taken out (`[h].first`) or a conversion (`h.to_s`) is left out too.
478
+ def note_returned(value, depth)
479
+ case value
480
+ when Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
481
+ Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
482
+ (@returned ||= []).push(CalleeWriteCensus.read_key(value, depth), value)
483
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
484
+ Prism::GlobalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
485
+ note_returned(value.value, depth)
486
+ when Prism::OrNode, Prism::AndNode
487
+ note_returned(value.left, depth)
488
+ note_returned(value.right, depth)
489
+ when Prism::ParenthesesNode then note_tail(value.body, depth)
490
+ end
491
+ end
492
+
493
+ # `written` with each parameter a returned value is itself ({#note_returned}), unless the def rebound it first.
494
+ def returned_parameters(written, table, seeds, rebinds)
495
+ CalleeWriteCensus.each_record(@returned, 2) do |key, node|
496
+ parameters = table.dig(key, :same)
497
+ written = (written || Set.new).merge(parameters) if parameters
498
+ next unless CalleeWriteCensus.seed(key, seeds) == :same
499
+ next if CalleeWriteCensus.rebound?(rebinds, key.first, node)
500
+
501
+ (written ||= Set.new) << key.first
502
+ end
503
+ written
504
+ end
505
+ end
506
+ private_constant :Returns
507
+
508
+ # Issue #2336 — how {Walk} reads a hand-off to code it cannot see: an opaque sink's arguments, and a call inside
509
+ # a block that is a method body.
510
+ module OpaqueSinks
511
+ private
512
+
513
+ def visit_counted(child, depth, counter)
514
+ instance_variable_set(counter, instance_variable_get(counter) + 1)
515
+ visit(child, depth)
516
+ ensure
517
+ instance_variable_set(counter, instance_variable_get(counter) - 1)
518
+ end
519
+
520
+ # True for a call whose block runs later as a method body (`foreign_calls`: `define_method`, ...).
521
+ def foreign_block?(node)
522
+ node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode) && @foreign_calls.include?(node.name)
523
+ end
524
+
525
+ # True for a call whose arguments the walk reads as written ({CalleeWriteCensus}, issue #2336).
526
+ def opaque_argument_call?(node)
527
+ name = node.name
528
+ return true if @foreign.positive?
529
+ return !CalleeWriteCensus.literal_send?(node) if SENDS.include?(name)
530
+ return false unless OPAQUE_ARGUMENTS.include?(name)
531
+
532
+ # A receiverless `call` is a self-call to a def of that name, which the hand-off walk resolves; the `*_exec`
533
+ # calls run their block whatever the receiver.
534
+ name != :call || !self_receiver?(node)
535
+ end
536
+
537
+ # Records each argument of `node` (a call or a `yield`) as written, through any container: the code that
538
+ # receives it is not seen.
539
+ def opaque_arguments(node, depth)
540
+ node.arguments&.arguments&.each do |argument|
541
+ case argument
542
+ when Prism::KeywordHashNode
543
+ argument.elements.each do |element|
544
+ value = element.value
545
+ value.nil? ? anonymous_argument(ANONYMOUS_KEYWORD_REST) : opaque_argument(value, depth, node)
546
+ end
547
+ when Prism::SplatNode
548
+ expression = argument.expression
549
+ expression.nil? ? anonymous_argument(ANONYMOUS_REST) : opaque_argument(expression, depth, node)
550
+ when Prism::ForwardingArgumentsNode then ANONYMOUS_FORWARDED.each { |rest| anonymous_argument(rest) }
551
+ else opaque_argument(argument, depth, node)
552
+ end
553
+ end
554
+ end
555
+
556
+ # An anonymous rest (`*`), keyword rest (`**`) or `...` handed on: every argument it carries is written.
557
+ def anonymous_argument(rest) = (@anonymous ||= Set.new) << rest
558
+
559
+ def opaque_argument(value, depth, node)
560
+ CalleeWriteCensus.each_record(sources(value, depth, nil, ID), 2) do |key, _step|
561
+ (@mutations ||= []).push(key, node, true, ANY)
562
+ end
563
+ end
564
+ end
565
+ private_constant :OpaqueSinks
566
+
567
+ # One pass over a body, collecting alias edges and mutation sites; {#census} closes them.
568
+ class Walk
569
+ include ValueSources
570
+ include BlockBindings
571
+ include Returns
572
+ include OpaqueSinks
573
+
574
+ def initialize(tables, returns: false)
575
+ @mutators = tables.mutators
576
+ @handled = tables.handled
577
+ @fresh = tables.fresh
578
+ @copiers = tables.copiers
579
+ @foreign_calls = tables.foreign_calls
580
+ @returns = returns
581
+ @edges = nil
582
+ @mutations = nil
583
+ @returned = nil
584
+ @anonymous = nil
585
+ @foreign = 0
586
+ @lambdas = 0
587
+ end
588
+
589
+ # {ValueSources#sources} of `node` at `depth`, as `node` itself: the flat `key, step` list, or nil.
590
+ def value_sources(node, depth) = sources(node, depth, nil, ID)
591
+
592
+ # A nested def is a scope of its own; a block and a lambda are entered one level deeper. The block of a
593
+ # foreign-self call ({#foreign_block?}) and a lambda are counted as they are entered.
594
+ def visit(node, depth)
595
+ return if node.nil? || node.is_a?(Prism::DefNode)
596
+
597
+ note(node, depth)
598
+ foreign = foreign_block?(node)
599
+ node.rigor_each_child do |child|
600
+ if child.is_a?(Prism::LambdaNode) then visit_counted(child, depth + 1, :@lambdas)
601
+ elsif foreign && child.is_a?(Prism::BlockNode) then visit_counted(child, depth + 1, :@foreign)
602
+ else visit(child, child.is_a?(Prism::BlockNode) ? depth + 1 : depth)
603
+ end
604
+ end
605
+ end
606
+
607
+ # Every parameter in `seeds` some mutation reaches.
608
+ def census(seeds, rebinds)
609
+ return EMPTY if @edges.nil? && @mutations.nil? && @returned.nil? && @anonymous.nil?
610
+
611
+ table = close(seeds, rebinds)
612
+ written = written_parameters(table, seeds, rebinds)
613
+ written = returned_parameters(written, table, seeds, rebinds) if @returned
614
+ written = (written || Set.new).merge(@anonymous) if @anonymous
615
+ return EMPTY if written.nil? && table.empty?
616
+
617
+ Census.new(written: written.nil? ? NONE : written.to_a.freeze, alias_table: table.freeze,
618
+ names: alias_names(table))
619
+ end
620
+
621
+ private
622
+
623
+ # The names {Census#aliases} can answer for: every key's name.
624
+ def alias_names(table)
625
+ names = Set.new
626
+ table.each_key { |key| names << key.first }
627
+ names.freeze
628
+ end
629
+
630
+ # The parameters the recorded mutations reach, or nil for none. A record is `key, node, counts_self, mode`.
631
+ def written_parameters(table, seeds, rebinds)
632
+ written = nil
633
+ CalleeWriteCensus.each_record(@mutations, 4) do |key, node, counts_self, mode|
634
+ written = merge_written(written, table[key], mode)
635
+ written = merge_written(written, table[IVAR_ANY_KEY], mode) if key.last == :ivar
636
+ status = CalleeWriteCensus.seed(key, seeds)
637
+ next if status.nil? || !counts_self || !CalleeWriteCensus.reaches?(mode, status)
638
+
639
+ (written ||= Set.new) << key.first unless CalleeWriteCensus.rebound?(rebinds, key.first, node)
640
+ end
641
+ written
642
+ end
643
+
644
+ def merge_written(written, by_status, mode)
645
+ by_status&.each do |status, parameters|
646
+ written = (written || Set.new).merge(parameters) if CalleeWriteCensus.reaches?(mode, status)
647
+ end
648
+ written
649
+ end
650
+
651
+ # `found` holds the sources flat, `key, step, key, step, ...` ({#sources}); a node that reads none records
652
+ # nothing, so the walk allocates only where a value may carry a parameter.
653
+ def note(node, depth)
654
+ case node
655
+ when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
656
+ found = sources(node.value, depth, nil, ID)
657
+ edge(found, node) { [node.name, depth - node.depth] } if found
658
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode,
659
+ Prism::InstanceVariableAndWriteNode
660
+ found = sources(node.value, depth, nil, ID)
661
+ edge(found, node) { [node.name, :ivar] } if found
662
+ when Prism::ClassVariableWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableAndWriteNode,
663
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode
664
+ found = sources(node.value, depth, nil, ID)
665
+ edge(found, node) { [node.name, :global] } if found
666
+ else note_other(node, depth)
667
+ end
668
+ end
669
+
670
+ def note_other(node, depth)
671
+ case node
672
+ when Prism::MultiWriteNode
673
+ found = sources(node.value, depth, nil, UNWRAP)
674
+ edge(found, node) { targets(node, depth) } if found
675
+ when Prism::ForNode
676
+ found = sources(node.collection, depth, nil, UNWRAP)
677
+ edge(found, node) { targets(node.index, depth) } if found
678
+ when Prism::CallNode then note_call(node, depth)
679
+ when Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexTargetNode
680
+ mutation(node.receiver, depth, node, nil)
681
+ when Prism::YieldNode then opaque_arguments(node, depth)
682
+ when Prism::ReturnNode then note_return(node, depth)
683
+ end
684
+ end
685
+
686
+ def note_call(node, depth)
687
+ name = node.name
688
+ mutation(node.receiver, depth, node, name) if @mutators.include?(name) || OPAQUE.include?(name)
689
+ opaque_arguments(node, depth) if opaque_argument_call?(node)
690
+ note_ivar_set(node, depth) if name == :instance_variable_set && self_receiver?(node)
691
+ case node.block
692
+ when Prism::BlockNode then note_block(node, depth)
693
+ when Prism::BlockArgumentNode then note_symbol_block(node, depth)
694
+ end
695
+ end
696
+
697
+ def note_ivar_set(node, depth)
698
+ arguments = node.arguments&.arguments
699
+ return unless arguments && arguments.size >= 2
700
+
701
+ found = sources(arguments[1], depth, nil, ID)
702
+ return if found.nil?
703
+
704
+ target = arguments.first
705
+ name = target.is_a?(Prism::SymbolNode) || target.is_a?(Prism::StringNode) ? target.unescaped.to_sym : nil
706
+ edge(found, node) { name ? [name, :ivar] : IVAR_ANY_KEY }
707
+ end
708
+
709
+ # `[h].each(&:clear)`: the receiver's elements, and so whatever it holds, take the mutator.
710
+ def note_symbol_block(node, depth)
711
+ symbol = node.block.expression
712
+ return unless symbol.is_a?(Prism::SymbolNode) && @mutators.include?(symbol.unescaped.to_sym)
713
+
714
+ CalleeWriteCensus.each_record(sources(node.receiver, depth, nil, ID), 2) do |key, _step|
715
+ (@mutations ||= []).push(key, node, true, ANY)
716
+ end
717
+ end
718
+
719
+ def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
720
+
721
+ # Records a mutation of whatever `receiver`, any expression, may be built from ({#sources}). The parameter read
722
+ # directly under a name the content scans count (`h << x`, `h[k] = v`; `name` nil for an index write) is theirs.
723
+ def mutation(receiver, depth, node, name)
724
+ found = sources(receiver, depth, nil, ID)
725
+ return if found.nil?
726
+
727
+ counts_self = !receiver.is_a?(Prism::LocalVariableReadNode) || !(name.nil? || @handled.include?(name))
728
+ CalleeWriteCensus.each_record(found, 2) do |key, step|
729
+ (@mutations ||= []).push(key, node, counts_self, step)
730
+ end
731
+ end
732
+
733
+ # Records that the targets the block answers (one key, or a list of keys) take the values `found` names. The
734
+ # edges are kept flat, `targets, found, node, ...`.
735
+ def edge(found, node)
736
+ targets = yield
737
+ (@edges ||= []).push(targets, found, node) unless targets.empty?
738
+ end
739
+
740
+ def targets(node, depth)
741
+ found = []
742
+ Source::NodeWalker.each(node) do |target|
743
+ case target
744
+ when Prism::LocalVariableTargetNode then found << [target.name, depth - target.depth]
745
+ when Prism::InstanceVariableTargetNode then found << [target.name, :ivar]
746
+ when Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode then found << [target.name, :global]
747
+ end
748
+ end
749
+ found
750
+ end
751
+
752
+ # `{ key => { status => Set[parameter] } }` closed over the edges. A read of a parameter itself has its seed's
753
+ # status unless the edge runs after the parameter is rebound. Each source merges straight into its targets'
754
+ # sets, so a pass that adds nothing allocates nothing.
755
+ def close(seeds, rebinds)
756
+ table = {}
757
+ return table if @edges.nil?
758
+
759
+ changed = true
760
+ while changed
761
+ changed = false
762
+ CalleeWriteCensus.each_record(@edges, 3) do |targets, found, node|
763
+ changed = true if close_edge(table, targets, found, node, seeds, rebinds)
764
+ end
765
+ end
766
+ table
767
+ end
768
+
769
+ # True when an edge's sources added a parameter under one of its targets.
770
+ def close_edge(table, targets, found, node, seeds, rebinds)
771
+ grew = false
772
+ CalleeWriteCensus.each_record(found, 2) do |key, step|
773
+ grew = true if flow(table, targets, step, table[key])
774
+ grew = true if key.last == :ivar && flow(table, targets, step, table[IVAR_ANY_KEY])
775
+ seed = CalleeWriteCensus.seed(key, seeds)
776
+ next if seed.nil? || CalleeWriteCensus.rebound?(rebinds, key.first, node)
777
+
778
+ status = CalleeWriteCensus.apply(step, seed)
779
+ each_target(targets) { |target| grew = true if stored?(table, target, status, key.first) }
780
+ end
781
+ grew
782
+ end
783
+
784
+ # Adds what `by_status` (a source's entry, nil: none) holds to each target through `step`. The statuses are read
785
+ # by name rather than by iterating the entry, which may be a target's own and grow meanwhile.
786
+ def flow(table, targets, step, by_status)
787
+ return false if by_status.nil?
788
+
789
+ grew = false
790
+ STATUSES.each do |status|
791
+ parameters = by_status[status]
792
+ next if parameters.nil?
793
+
794
+ reached = CalleeWriteCensus.apply(step, status)
795
+ each_target(targets) { |target| grew = true if stored?(table, target, reached, parameters) }
796
+ end
797
+ grew
798
+ end
799
+
800
+ # Yields each key of `targets`, one key (`[name, level]`, whose first is a Symbol) or a list of keys.
801
+ def each_target(targets, &)
802
+ targets.first.is_a?(Symbol) ? yield(targets) : targets.each(&)
803
+ end
804
+
805
+ # True when `parameters` (a set, or one name) added something under `target` and `status`.
806
+ def stored?(table, target, status, parameters)
807
+ set = ((table[target] ||= {})[status] ||= Set.new)
808
+ size = set.size
809
+ parameters.is_a?(Symbol) ? set << parameters : set.merge(parameters)
810
+ set.size != size
811
+ end
812
+ end
813
+ end
814
+ end
815
+ end