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
@@ -272,6 +272,8 @@ module Rigor
272
272
 
273
273
  def ivar_name?(name) = variable_kind(name) == :ivar
274
274
 
275
+ def local_name?(name) = variable_kind(name) == :local
276
+
275
277
  # Ruby spells a global with a leading `$`, a class variable with `@@`, an instance variable with a single
276
278
  # `@`, and a local with none of them.
277
279
  def variable_kind(name)
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "anonymous_meta_class"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2490 — a block, inside an instance body of a class `K`, whose `self` is provably not an instance of `K`, so
10
+ # an `@x` write directly in it writes some other object's ivar and the class-ivar censuses must not credit it to
11
+ # `K`. Read syntactically, before any type is known, by both the per-file census ({ScopeIndexer}) and the cross-file
12
+ # {IvarWriterCensus}. False positives come first, so a receiver that may hold a `K` instance keeps the credit:
13
+ #
14
+ # - `instance_eval` / `instance_exec` on a receiver that is provably no `K` instance ({.provably_other?}): a
15
+ # constant or `self.class` (a class object), `X.new` / `X.allocate` on a constant `X` not spelling `K`, a
16
+ # literal. Any other receiver (a parameter, a local, `dup`, `itself`) may be a `K` and still counts;
17
+ # - `class_eval` / `module_eval` / `class_exec` / `module_exec` on an explicit receiver: `self` is the receiver
18
+ # module, so `@x` directly in the block is that module object's own ivar;
19
+ # - `define_method` on a constant not spelling `K` (its block runs on that class's instances), and
20
+ # `define_singleton_method` on a class object or a literal;
21
+ # - a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block: `self` is the new class.
22
+ #
23
+ # `class << obj` inside an instance body is one more barred body; the callers bar it themselves, since it is no
24
+ # call. Only a barred body's own statements are dropped: a `define_method` block nested in a `class_eval` or
25
+ # `class << obj` body whose module may be `K` or an instance's singleton class runs on a `K` instance and still
26
+ # counts, and so does a receiver-less `def` nested in either (#2941, {.reentry_blocks}).
27
+ # `def.ivar-write-mismatch`'s collector reads the same rule (#2943, {.census_children}).
28
+ module CensusForeignBlock
29
+ INSTANCE_EVALS = %i[instance_eval instance_exec].to_set.freeze
30
+ MODULE_EVALS = %i[class_eval module_eval class_exec module_exec].to_set.freeze
31
+ DEFINERS = %i[define_method define_singleton_method].to_set.freeze
32
+ SEND_CALLS = %i[send public_send __send__].to_set.freeze
33
+ # The constructors whose receiver names the class of the object they answer.
34
+ CONSTRUCTORS = %i[new allocate].to_set.freeze
35
+ # The literal receivers, none of them an instance of a project class.
36
+ LITERALS = [Prism::StringNode, Prism::InterpolatedStringNode, Prism::SymbolNode, Prism::IntegerNode,
37
+ Prism::FloatNode, Prism::ArrayNode, Prism::HashNode, Prism::RangeNode, Prism::RegularExpressionNode,
38
+ Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::LambdaNode].freeze
39
+ # The calls that answer a module of their receiver.
40
+ MODULE_OF_INSTANCE = %i[class singleton_class].to_set.freeze
41
+ # The constants naming a module every class has among its ancestors, whose methods an instance runs.
42
+ ANCESTOR_ROOTS = %w[Object BasicObject Kernel].to_set.freeze
43
+ # The calls whose answer is foreign when their receiver is: the object itself, a copy, a new instance, its class.
44
+ FOREIGN_PRESERVING = %i[new allocate dup clone itself class singleton_class].to_set.freeze
45
+ # Issue #2983 — the scalar literals, whose blockless calls answer no instance of a project class, unlike a
46
+ # container's (`[k].first`) or a lambda's.
47
+ SCALAR_LITERALS = (LITERALS - [Prism::ArrayNode, Prism::HashNode, Prism::LambdaNode]).freeze
48
+ # A String or Symbol literal that spells a constant path, which a call may resolve to the class
49
+ # (`"K".constantize`).
50
+ CONSTANT_SPELLING = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
51
+ # Separates the qualifications of a {.candidate_key}; no constant name holds it.
52
+ CANDIDATE_SEPARATOR = "|"
53
+ # Marks a candidate key whose code runs on the constant's own value's module ({.root_constant}).
54
+ OBJECT_MARK = "@"
55
+ COPIES = %i[dup clone itself].to_set.freeze
56
+ BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
57
+ NONE = [].freeze
58
+ # Every name {.foreign?} can answer true for, so any other call returns at one Set probe.
59
+ CALLS = (INSTANCE_EVALS | MODULE_EVALS | DEFINERS | %i[new define]).freeze
60
+
61
+ module_function
62
+
63
+ # Whether `node`'s literal block runs with a `self` that is provably not an instance of `class_name`.
64
+ def foreign?(node, class_name)
65
+ unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name) && node.block.is_a?(Prism::BlockNode)
66
+ return false
67
+ end
68
+ return true if AnonymousMetaClass.block_form_receiver(node)
69
+
70
+ name = node.name
71
+ receiver = node.receiver
72
+ return false if self_node?(receiver)
73
+ return true if MODULE_EVALS.include?(name)
74
+ return provably_other?(receiver, class_name) if INSTANCE_EVALS.include?(name)
75
+ return provably_other?(receiver, class_name) if name == :define_singleton_method
76
+
77
+ foreign_constant?(receiver, class_name)
78
+ end
79
+
80
+ # A receiver that is provably no instance of `class_name`: a constant or `self.class` (a class object, `K`
81
+ # itself included), `X.new` / `X.allocate` on a constant `X` not spelling `class_name`, or a literal.
82
+ def provably_other?(receiver, class_name)
83
+ case receiver
84
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then true
85
+ when Prism::CallNode
86
+ return receiver.arguments.nil? if receiver.name == :class
87
+
88
+ CONSTRUCTORS.include?(receiver.name) && foreign_constant?(receiver.receiver, class_name)
89
+ else LITERALS.any? { |klass| receiver.is_a?(klass) }
90
+ end
91
+ end
92
+
93
+ # A constant that does not spell `class_name`.
94
+ def foreign_constant?(node, class_name)
95
+ (node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)) && !own_class?(node, class_name)
96
+ end
97
+
98
+ # The children of `node` a census walk for `class_name` still descends into when `node` runs code whose `self`
99
+ # is another object, or nil for an ordinary node: a {.foreign?} call keeps its receiver and arguments, and a
100
+ # `class << obj` body its expression; both keep the code nested in them that runs on a `class_name` instance
101
+ # again ({.reentry_blocks}) unless `reentry` is false, as it is in a singleton-side `def`, whose `self.class`
102
+ # and `singleton_class` are no module of an instance. A reentered `def` comes back as the {Prism::DefNode},
103
+ # which a walk that stops at a `def` skips and the class-ivar census walks into (#2941).
104
+ def census_children(node, class_name, reentry: true)
105
+ case node
106
+ when Prism::SingletonClassNode
107
+ return [node.expression] unless reentry
108
+
109
+ [node.expression, *reentry_blocks(node, class_name)]
110
+ when Prism::CallNode
111
+ return nil unless foreign?(node, class_name)
112
+ return [node.receiver, node.arguments].compact unless reentry
113
+
114
+ [node.receiver, node.arguments, *reentry_blocks(node, class_name)].compact
115
+ end
116
+ end
117
+
118
+ # The code inside barred code whose `self` may again be an instance of `class_name`, in a `class_eval`-family
119
+ # block whose module may be `class_name` or the singleton class of one of its instances ({.foreign_module?}) or in
120
+ # a `class << obj` body on an operand {.provably_other?} does not name:
121
+ #
122
+ # - a `define_method` block on the implicit or `self` receiver, called directly or through `send` /
123
+ # `public_send` / `__send__` with the literal name;
124
+ # - a receiver-less `def` (#2941), which defines an instance method of that module, or a singleton method of
125
+ # the `class << obj` operand, just as the `define_method` does.
126
+ #
127
+ # Ruby runs each on a `K` instance (a `K` method, or a singleton method of the instance), so its writes are
128
+ # `K`'s; an untyped receiver keeps the credit, as {.foreign?} keeps it for `instance_eval`.
129
+ def reentry_blocks(node, class_name)
130
+ body =
131
+ case node
132
+ when Prism::SingletonClassNode then node.body unless provably_other?(node.expression, class_name)
133
+ when Prism::CallNode
134
+ node.block.body if MODULE_EVALS.include?(node.name) && !foreign_module?(node.receiver, class_name)
135
+ end
136
+ return NONE if body.nil?
137
+
138
+ found = []
139
+ collect_define_method_blocks(body, found)
140
+ found
141
+ end
142
+
143
+ # A `class_eval` receiver whose module is provably neither `class_name`, an ancestor of it nor an instance's
144
+ # singleton class, read from inside an instance body. A receiver chain is foreign only when it is:
145
+ #
146
+ # - rooted at a constant other than `class_name`'s own spelling and {ANCESTOR_ROOTS} (`Other.class_eval`,
147
+ # `Other.dup`), a `X.new` or `Class.new` on such a constant among them, or rooted at a literal, through
148
+ # {FOREIGN_PRESERVING} calls only;
149
+ # - `.class` / `.singleton_class` on a receiver {.provably_other?} names (`Other.new.class`, `"s".class`,
150
+ # `self.class.class`);
151
+ # - rooted at a scalar literal through blockless calls of any name (`"s".upcase.class`, #2983), unless the
152
+ # literal is a String or Symbol that spells a constant (`"K".constantize`).
153
+ #
154
+ # A `superclass` call may answer an ancestor (`Other.superclass` is `Object`), a `const_get` or registry call may
155
+ # answer the class (`Other.const_get(:K)`), and every other receiver
156
+ # (`self.class`, `singleton_class`, a parameter's `o.class`, `self.class.base_class`) may name `class_name`, so
157
+ # they keep the credit. Parentheses are looked through. The rule is syntactic: a constant naming another
158
+ # ancestor of the class (`Base.class_eval` with `K < Base`), an alias of it or one of its instances reads as
159
+ # foreign here, and the cross-file census files the barred block's code under that constant instead
160
+ # ({.root_constant}), whose entries the class's census joins (#2983).
161
+ def foreign_module?(receiver, class_name)
162
+ receiver = unwrap(receiver)
163
+ return true if scalar_literal_rooted?(receiver)
164
+
165
+ case receiver
166
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
167
+ foreign_constant?(receiver, class_name) && !ANCESTOR_ROOTS.include?(receiver.slice.delete_prefix("::"))
168
+ when Prism::CallNode then foreign_module_call?(receiver, class_name)
169
+ else LITERALS.any? { |klass| receiver.is_a?(klass) }
170
+ end
171
+ end
172
+
173
+ # A call in the chain keeps it foreign only when it answers its receiver's own object, a copy, a new instance
174
+ # or its class ({FOREIGN_PRESERVING}); any other call (`const_get`, a registry's `fetch`, `superclass`) may
175
+ # answer the class, so the receiver may be it.
176
+ def foreign_module_call?(call, class_name)
177
+ if MODULE_OF_INSTANCE.include?(call.name) && call.arguments.nil? &&
178
+ provably_other?(unwrap(call.receiver), class_name)
179
+ return true
180
+ end
181
+ return false unless FOREIGN_PRESERVING.include?(call.name)
182
+
183
+ foreign_module?(call.receiver, class_name)
184
+ end
185
+
186
+ # Issue #2983 — whether `receiver` is a chain of blockless calls rooted at a scalar literal that spells no
187
+ # constant: whatever the calls are, Ruby's methods on such a literal answer no project class or its instance.
188
+ def scalar_literal_rooted?(receiver)
189
+ node = receiver
190
+ while node.is_a?(Prism::CallNode)
191
+ return false if node.block
192
+
193
+ node = unwrap(node.receiver)
194
+ end
195
+ return false if node.equal?(receiver) || SCALAR_LITERALS.none? { |klass| node.is_a?(klass) }
196
+
197
+ !((node.is_a?(Prism::StringNode) || node.is_a?(Prism::SymbolNode)) && CONSTANT_SPELLING.match?(node.unescaped))
198
+ end
199
+
200
+ # Issue #2983 — the constant a `class_eval` receiver chain is rooted at through {FOREIGN_PRESERVING} calls
201
+ # (`Anc`, `INST.class`, `AliasK.new.class`, `Other.singleton_class`), as its candidate key under `nesting`
202
+ # ({.candidate_key}), or nil. The module such a chain names is the constant's, its class's or its singleton's,
203
+ # so the code a barred block reenters runs on the constant's instances or on the object it holds; the
204
+ # cross-file census files it under the key, and a class whose census joins the constant the key resolves to (an
205
+ # ancestor, an alias, an instance constant of it) sees the write.
206
+ #
207
+ # Review round 2 of #3001 — a `.class` or `.singleton_class` taken of the constant's own value (`INST.class`,
208
+ # `X.singleton_class`, through `dup` / `clone` / `itself`) names the module of that object, which is a class
209
+ # instance only when the constant binds one; the key then carries {OBJECT_MARK}, and the join credits it only
210
+ # when the constant resolves to an instance binding (`INST = K.new`). One taken of a class object (`X.class` of a
211
+ # `.class`, `X.new.class.singleton_class`) names `Class` or a metaclass, whose methods no instance runs: nil.
212
+ def root_constant(receiver, nesting)
213
+ node = unwrap(receiver)
214
+ object = false
215
+ while node.is_a?(Prism::CallNode) && FOREIGN_PRESERVING.include?(node.name)
216
+ inner = unwrap(node.receiver)
217
+ if MODULE_OF_INSTANCE.include?(node.name)
218
+ value = through_copies(inner)
219
+ return if value.is_a?(Prism::CallNode) && MODULE_OF_INSTANCE.include?(value.name)
220
+
221
+ object = true if constant_node?(value)
222
+ end
223
+ node = inner
224
+ end
225
+ return unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
226
+
227
+ key = candidate_key(node.slice, nesting)
228
+ object ? "#{OBJECT_MARK}#{key}" : key
229
+ end
230
+
231
+ # `node` with `dup`, `clone` and `itself` calls looked through: the object they answer has the same class.
232
+ def through_copies(node)
233
+ node = unwrap(node.receiver) while node.is_a?(Prism::CallNode) && COPIES.include?(node.name)
234
+ node
235
+ end
236
+
237
+ def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
238
+
239
+ # Review round 1 of #3001 — the qualifications a constant spelled `spelling` may have under the lexical
240
+ # `nesting` (the enclosing class and module names), nearest first as Ruby's lookup tries them, joined by
241
+ # {CANDIDATE_SEPARATOR}: `Anc` under `N::K` is `"N::K::Anc|N::Anc|Anc"`. A rooted `::Anc`, or one at the top
242
+ # level, has the one candidate `"Anc"`. `ScopeIndexer::IvarContributions` resolves the key to the first
243
+ # candidate the project declares.
244
+ def candidate_key(spelling, nesting)
245
+ return spelling.delete_prefix("::") if spelling.start_with?("::") || nesting.empty?
246
+
247
+ nesting.size.downto(0).map { |depth| [*nesting.first(depth), spelling].join("::") }.join(CANDIDATE_SEPARATOR)
248
+ end
249
+
250
+ # Review round 3 of #3001 — whether `call` is `Class.new`, `Module.new`, `Struct.new` or `Data.define` on the
251
+ # bare constant, whose answer is a class.
252
+ def meta_new?(call)
253
+ !AnonymousMetaClass::META_NEW_SELECTORS[AnonymousMetaClass.literal_constant_name(call.receiver)].nil?
254
+ end
255
+
256
+ # Issue #2983 — for a constant write `C = K` or `C = K.new` / `K.allocate` under the lexical `nesting`, the
257
+ # census entry `[:binds_class, key, name, kind]`: `K`'s {.candidate_key}, `C`'s qualified name (a path write's
258
+ # as written) and `:instance` for a constructor or `:alias`, else nil. `C` is an alias or an instance of the
259
+ # class `K` resolves to.
260
+ def binding_entry(node, nesting)
261
+ value = unwrap(node.value)
262
+ instance = value.is_a?(Prism::CallNode) && CONSTRUCTORS.include?(value.name) && value.block.nil?
263
+ # Review round 3 of #3001: `Struct.new(:a)`, `Class.new(Base)` and `Module.new` answer a class, not an instance.
264
+ # A guard beside `IvarContributions.module?`, which drops the same credit once the constant is a declared class.
265
+ return if instance && meta_new?(value)
266
+
267
+ value = value.receiver if instance
268
+ return unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
269
+
270
+ name = node.is_a?(Prism::ConstantWriteNode) ? [*nesting, node.name].join("::") : node.target.slice
271
+ [:binds_class, candidate_key(value.slice, nesting), name.delete_prefix("::"),
272
+ instance ? :instance : :alias].freeze
273
+ end
274
+
275
+ # Issue #2983 — the module a String `class_eval` / `module_eval` may define methods on, for
276
+ # `IvarWriterCensus#record_string_eval`: `owner`, the class owning a class body, for an implicit or `self`
277
+ # receiver there (one in an instance body raises); `instance`, the instance body's class, for a receiver
278
+ # {.foreign_module?} does not bar; otherwise the receiver chain's root constant ({.root_constant}); or nil.
279
+ def string_eval_target(node, instance, owner)
280
+ return (owner unless instance) if self_node?(node.receiver)
281
+ return instance if instance && !foreign_module?(node.receiver, instance)
282
+
283
+ nesting = instance || owner
284
+ root_constant(node.receiver, nesting ? nesting.split("::") : NONE)
285
+ end
286
+
287
+ # For a barred `class_eval`-family block whose receiver {.foreign_module?} takes as another module and whose
288
+ # chain is rooted at a constant ({.root_constant}), that constant's candidate key and the code in the block that
289
+ # runs on its module's instances, as {.reentry_blocks} collects it; else nil.
290
+ def rooted_reentry(node, class_name)
291
+ return unless MODULE_EVALS.include?(node.name) && foreign_module?(node.receiver, class_name)
292
+
293
+ root = root_constant(node.receiver, class_name.split("::"))
294
+ return if root.nil?
295
+
296
+ found = []
297
+ collect_define_method_blocks(node.block.body, found)
298
+ [root, found]
299
+ end
300
+
301
+ # `node` with any parentheses around a single expression looked through.
302
+ def unwrap(node)
303
+ while node.is_a?(Prism::ParenthesesNode)
304
+ body = node.body
305
+ body = body.body.first if body.is_a?(Prism::StatementsNode) && body.body.size == 1
306
+ break if body.nil? || body.is_a?(Prism::StatementsNode)
307
+
308
+ node = body
309
+ end
310
+ node
311
+ end
312
+
313
+ def collect_define_method_blocks(node, found)
314
+ return unless node.is_a?(Prism::Node)
315
+ return found << node if node.is_a?(Prism::DefNode) && node.receiver.nil?
316
+ return if BARRIERS.any? { |klass| node.is_a?(klass) }
317
+
318
+ if node.is_a?(Prism::CallNode)
319
+ return found << node.block if define_method_call?(node)
320
+ return if node.block.is_a?(Prism::BlockNode) && CALLS.include?(node.name)
321
+ end
322
+
323
+ node.rigor_each_child { |child| collect_define_method_blocks(child, found) }
324
+ end
325
+
326
+ # A `define_method` with a literal block on the implicit or `self` receiver, called directly or through a `send`
327
+ # whose first argument is the literal name.
328
+ def define_method_call?(node)
329
+ return false unless node.is_a?(Prism::CallNode) && self_node?(node.receiver)
330
+ return false unless node.block.is_a?(Prism::BlockNode)
331
+ return true if node.name == :define_method
332
+ return false unless SEND_CALLS.include?(node.name)
333
+
334
+ first = node.arguments&.arguments&.first
335
+ (first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)) && first.unescaped == "define_method"
336
+ end
337
+
338
+ # `self.class`, or a constant spelling `class_name`.
339
+ def own_class?(node, class_name)
340
+ case node
341
+ when Prism::CallNode
342
+ node.name == :class && self_node?(node.receiver) && node.arguments.nil?
343
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
344
+ spelled = node.slice.delete_prefix("::")
345
+ spelled == class_name || spelled == class_name.split("::").last
346
+ else
347
+ false
348
+ end
349
+ end
350
+
351
+ def self_node?(node) = node.nil? || node.is_a?(Prism::SelfNode)
352
+ end
353
+ end
354
+ end
@@ -57,7 +57,7 @@ module Rigor
57
57
  widened = names.reduce(scope) do |acc, name|
58
58
  ivar = name.start_with?("@")
59
59
  current = ivar ? scope.ivar(name) : scope.local(name)
60
- next acc if current.nil? || current == untyped
60
+ next acc if current.nil? || Type::Combinator.untyped?(current)
61
61
 
62
62
  ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
63
63
  end
@@ -3,6 +3,7 @@
3
3
  require_relative "../type"
4
4
  require_relative "../reflection"
5
5
  require_relative "external_ancestor_resolution"
6
+ require_relative "../memo_audit"
6
7
  require_relative "project_method_ownership"
7
8
 
8
9
  module Rigor
@@ -185,9 +186,15 @@ module Rigor
185
186
  return false unless scope.known_user_class?(class_name)
186
187
 
187
188
  by_method = (ProjectMethodOwnership.memo(scope)[:ancestry][class_name] ||= {})
188
- return by_method[method_sym] if by_method.key?(method_sym)
189
+ if by_method.key?(method_sym)
190
+ return MemoAudit.check("ancestry non-escaping", by_method[method_sym], class_name, method_sym) do
191
+ compute_ancestry_non_escaping?(class_name, method_sym, scope)
192
+ end
193
+ end
189
194
 
190
- by_method[method_sym] = compute_ancestry_non_escaping?(class_name, method_sym, scope)
195
+ by_method[method_sym] = MemoAudit.key_audit("ancestry non-escaping") do
196
+ compute_ancestry_non_escaping?(class_name, method_sym, scope)
197
+ end
191
198
  end
192
199
 
193
200
  def compute_ancestry_non_escaping?(class_name, method_sym, scope)
@@ -236,7 +243,7 @@ module Rigor
236
243
  OBJECT_NON_ESCAPING = %i[tap then yield_self].freeze
237
244
 
238
245
  ENUMERABLE_NON_ESCAPING = %i[
239
- each map collect flat_map collect_concat
246
+ each reverse_each map collect flat_map collect_concat
240
247
  select filter reject filter_map
241
248
  find detect find_index find_all
242
249
  any? all? none? one? count tally sum
@@ -248,7 +255,7 @@ module Rigor
248
255
  zip
249
256
  ].freeze
250
257
 
251
- ARRAY_EXTRA = %i[each_index].freeze
258
+ ARRAY_EXTRA = %i[each_index rfind index rindex].freeze
252
259
  HASH_EXTRA = %i[
253
260
  each_pair each_key each_value
254
261
  transform_keys transform_values
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../source/node_walker"
5
+ require_relative "../source/node_children"
6
+ require_relative "../source/literals"
7
+ require_relative "../typing_state"
8
+ require_relative "../memo_audit"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # Issue #2555 L3 — which locals of a body may have ESCAPED, so a write to them never carries the `:synthesised`
13
+ # construction fact ({Scope#construction_fact?}).
14
+ #
15
+ # The fact lets the mutation seams re-grow a carrier without the `Dynamic[top]` floor, and the floor existed
16
+ # because a store the seam does not see leaves the carrier stale. A store reaches a local's object unseen only
17
+ # through another holder of it, and almost any position can make one: an alias, a container, an argument (a
18
+ # constructor or an attribute writer keeps it, `Array(a)` and `ident(a)` return it), a call that returns the
19
+ # receiver for some class (`Set#merge`, `a.each`), or a method the project defines. So every reference escapes
20
+ # unless it is in one of these positions:
21
+ #
22
+ # - the receiver of a call on the per-class {READERS} list, which neither returns, keeps nor mutates its receiver;
23
+ # - the receiver of an {ITERATORS} call or of a mutator the seams model ({.mutators_for}), with the call's value
24
+ # discarded;
25
+ # - the body's value position (its last statement, or `return a`), after which nothing in the body runs;
26
+ # - a bare read whose value is discarded.
27
+ #
28
+ # A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
29
+ # class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
30
+ # any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
31
+ # body that reaches the frame reflectively: a call of `binding`, `eval`, `local_variable_get`,
32
+ # `local_variable_set`, `instance_variable_set` or a string-form `instance_eval` / `class_eval` /
33
+ # `module_eval`, a `send` / `__send__` / `public_send` dispatch whose first argument is one of those names
34
+ # as a literal Symbol or String, or a `TOPLEVEL_BINDING` reference.
35
+ # The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
36
+ # the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
37
+ # scanned too, though its locals are its own; a same-named local there can only over-report an escape.
38
+ module ConstructionEscape
39
+ CLASSES = %w[Array Hash Set].freeze
40
+
41
+ # The calls that answer a value other than the receiver, keep no reference to it, and change nothing in it.
42
+ READERS = Ractor.make_shareable(
43
+ {
44
+ "Array" => %i[
45
+ size length count empty? any? none? all? include? first last [] dig fetch index find_index sum min max
46
+ join dup map select filter reject sort sort_by uniq compact flatten reverse take drop to_h to_set ==
47
+ ].to_set,
48
+ "Hash" => %i[
49
+ size length count empty? any? include? key? has_key? member? value? has_value? [] dig fetch keys values
50
+ dup map select filter reject to_a ==
51
+ ].to_set,
52
+ "Set" => %i[size length count empty? any? include? member? first dup map select filter reject to_a ==].to_set
53
+ }
54
+ )
55
+
56
+ # The calls that answer the receiver itself and yield only its members: safe with their value discarded.
57
+ ITERATORS = Ractor.make_shareable(
58
+ {
59
+ "Array" => %i[each each_with_index each_index reverse_each].to_set,
60
+ "Hash" => %i[each each_pair each_key each_value each_with_index].to_set,
61
+ "Set" => %i[each].to_set
62
+ }
63
+ )
64
+
65
+ # Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
66
+ FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
67
+ STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
68
+ # The spellings that reach a {FRAME_CALLS} or {STRING_EVAL_CALLS} name through a literal Symbol or String
69
+ # argument (`send(:eval, …)`, `send("eval", …)`).
70
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
71
+
72
+ CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
73
+ LOCAL_REFERENCES = [
74
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
75
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
76
+ ].freeze
77
+ LOCAL_WRITES = [
78
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
79
+ Prism::LocalVariableAndWriteNode
80
+ ].freeze
81
+ ALL = :all
82
+ EMPTY_CALLS = Set.new.freeze
83
+ # ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so a long-lived process never serves a verdict from
84
+ # a body an earlier run parsed: keyed by body node, the only input of the scan.
85
+ CACHE = TypingState.memo(
86
+ :construction_escape,
87
+ run_scoped: true, audit: "construction escape",
88
+ reason: "keyed by the body node; the escape scan is a pure function of the body"
89
+ )
90
+ private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE,
91
+ :STRING_EVAL_CALLS, :SEND_CALLS
92
+
93
+ # One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
94
+ Result = Data.define(:escapes, :calls)
95
+ private_constant :Result
96
+
97
+ module_function
98
+
99
+ # True when the local `name`, bound to a carrier of `class_name`, may escape somewhere in `body`, or when
100
+ # `body` is unknown. `scope` answers whether the project defines one of the admitted calls on the class
101
+ # ({.project_defines?}).
102
+ def escaped?(body, name, class_name, scope)
103
+ return true if body.nil? || class_name.nil?
104
+
105
+ result = escape_scan(body)
106
+ return true if result.equal?(ALL)
107
+
108
+ reason = result.escapes[name]
109
+ return true if !reason.nil? && (reason.equal?(ALL) || reason.include?(class_name))
110
+
111
+ result.calls.fetch(name, EMPTY_CALLS).any? { |call| project_defines?(scope, class_name, call) }
112
+ end
113
+
114
+ # Whether the project defines a method named `call` anywhere ({Scope#project_method_name?}): on any class or
115
+ # module, instance or singleton side, in a `refine` block, in any file. An admitted call of such a name escapes
116
+ # the local whatever the receiver's class, because a prepended or included module, a refinement or a reopen can
117
+ # put that name on Array, Hash or Set, and telling which owner reaches the receiver is not worth the precision
118
+ # it buys: without the fact the seams answer as before. When no def is found the def-node lookup on the class
119
+ # itself still runs, so an incremental run records the missing `Array#size` and a later reopen re-analyses
120
+ # this file.
121
+ def project_defines?(scope, class_name, call)
122
+ scope.project_method_name?(call) || !scope.user_def_for(class_name, call).nil?
123
+ end
124
+
125
+ # The receiver mutators the seams model for `class_name` ({MutationWidening}, {HashLookupMutation},
126
+ # {SetMutation}).
127
+ def mutators_for(class_name)
128
+ case class_name
129
+ when "Array" then MutationWidening::ARRAY_MUTATORS
130
+ when "Hash" then MutationWidening::HASH_MUTATORS | HashLookupMutation::MUTATORS
131
+ else SetMutation::MUTATORS
132
+ end
133
+ end
134
+
135
+ # {.scan} of `body`, memoised for the run.
136
+ def escape_scan(body)
137
+ return scan(body) if MemoAudit.enabled? && MemoAudit.suspended?
138
+
139
+ table = TypingState.current.weak_key_map(CACHE)
140
+ stored = table[body]
141
+ return table[body] = MemoAudit.key_audit("construction escape") { scan(body) } if stored.nil?
142
+
143
+ MemoAudit.check("construction escape", stored, body) { scan(body) }
144
+ end
145
+
146
+ def scan(body)
147
+ escapes = {}
148
+ calls = {}
149
+ Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
150
+ return ALL if frame_access?(node)
151
+ next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
152
+
153
+ record(escapes, calls, node, ancestors)
154
+ end
155
+ Result.new(escapes: escapes.freeze, calls: calls.freeze)
156
+ end
157
+
158
+ def record(escapes, calls, node, ancestors)
159
+ name = node.name
160
+ call = receiver_call(node, ancestors)
161
+ unsafe =
162
+ if call
163
+ unsafe_classes(call, ancestors).tap { |u| (calls[name] ||= Set.new) << call.name if u.size < CLASSES.size }
164
+ else
165
+ escape_reference?(node, ancestors) ? CLASSES : []
166
+ end
167
+ return if unsafe.empty? || escapes[name].equal?(ALL)
168
+
169
+ escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
170
+ end
171
+
172
+ # A node that reaches the frame's locals by name: a {FRAME_CALLS} or string-eval call, a `send`-family
173
+ # dispatch on a literal frame-call name (`send(:eval, …)`), or a `TOPLEVEL_BINDING` reference under any
174
+ # qualification (a project's own `X::TOPLEVEL_BINDING` escapes too, costing only precision), whose
175
+ # `local_variable_get` / `local_variable_set` reads and rebinds the top frame.
176
+ def frame_access?(node)
177
+ case node
178
+ when Prism::CallNode then frame_call_name?(node)
179
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then node.name == :TOPLEVEL_BINDING
180
+ else false
181
+ end
182
+ end
183
+
184
+ def frame_call_name?(node)
185
+ name = node.name
186
+ return true if FRAME_CALLS.include?(name)
187
+ return true if STRING_EVAL_CALLS.include?(name) && !node.arguments.nil?
188
+ return false unless SEND_CALLS.include?(name)
189
+
190
+ arguments = node.arguments&.arguments
191
+ return false if arguments.nil? || arguments.empty?
192
+
193
+ dispatched = Source::Literals.symbol_or_string(arguments.first)
194
+ FRAME_CALLS.include?(dispatched) ||
195
+ (STRING_EVAL_CALLS.include?(dispatched) && arguments.size > 1)
196
+ end
197
+
198
+ # The call a read outside any closure is the receiver of, or nil.
199
+ def receiver_call(node, ancestors)
200
+ return nil unless node.is_a?(Prism::LocalVariableReadNode) && !in_closure?(ancestors)
201
+
202
+ parent = ancestors.last
203
+ parent if parent.is_a?(Prism::CallNode) && parent.receiver.equal?(node)
204
+ end
205
+
206
+ # The classes for which `call` on the local may hand it on.
207
+ def unsafe_classes(call, ancestors)
208
+ discarded = !value_used?(ancestors, ancestors.size - 2, call)
209
+ CLASSES.reject do |class_name|
210
+ READERS.fetch(class_name).include?(call.name) ||
211
+ (discarded && (ITERATORS.fetch(class_name).include?(call.name) ||
212
+ mutators_for(class_name).include?(call.name)))
213
+ end
214
+ end
215
+
216
+ def in_closure?(ancestors)
217
+ ancestors.any? { |a| CLOSURES.any? { |k| a.is_a?(k) } }
218
+ end
219
+
220
+ # Whether a reference that is no call's receiver may hand the object on.
221
+ def escape_reference?(node, ancestors)
222
+ return true if in_closure?(ancestors)
223
+ return value_used?(ancestors, ancestors.size - 1, node) if LOCAL_WRITES.any? { |k| node.is_a?(k) }
224
+ return false unless node.is_a?(Prism::LocalVariableReadNode)
225
+
226
+ parent = ancestors.last
227
+ return false if parent.is_a?(Prism::ArgumentsNode) && ancestors[-2].is_a?(Prism::ReturnNode)
228
+
229
+ !(parent.is_a?(Prism::StatementsNode) && !value_used?(ancestors, ancestors.size - 1, node))
230
+ end
231
+
232
+ # Whether the value of `node`, whose parent is `ancestors[index]`, reaches anything. The body's own value (its
233
+ # last statement) is the return position, after which nothing in the body runs, so it reads as unused.
234
+ def value_used?(ancestors, index, node)
235
+ while index >= 0
236
+ parent = ancestors[index]
237
+ case parent
238
+ when Prism::StatementsNode
239
+ return false unless parent.body.last.equal?(node)
240
+ when Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode
241
+ return true if parent.predicate.equal?(node)
242
+ when Prism::WhenNode, Prism::InNode
243
+ return true unless parent.statements.equal?(node)
244
+ when Prism::WhileNode, Prism::UntilNode, Prism::EnsureNode
245
+ return !parent.statements.equal?(node)
246
+ when Prism::ElseNode, Prism::BeginNode, Prism::ParenthesesNode, Prism::ProgramNode, Prism::RescueNode
247
+ nil
248
+ else
249
+ return true
250
+ end
251
+ node = parent
252
+ index -= 1
253
+ end
254
+ false
255
+ end
256
+ end
257
+ end
258
+ end