rigortype 0.4.5 → 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 (452) 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 +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../source/node_walker"
5
+ require_relative "../source/node_children"
6
+ require_relative "../run_scoped_slots"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # Issue #2555 L3 — which locals of a body may have ESCAPED, so a write to them never carries the `:synthesised`
11
+ # construction fact ({Scope#construction_fact?}).
12
+ #
13
+ # The fact lets the mutation seams re-grow a carrier without the `Dynamic[top]` floor, and the floor existed
14
+ # because a store the seam does not see leaves the carrier stale. A store reaches a local's object unseen only
15
+ # through another holder of it, and almost any position can make one: an alias, a container, an argument (a
16
+ # constructor or an attribute writer keeps it, `Array(a)` and `ident(a)` return it), a call that returns the
17
+ # receiver for some class (`Set#merge`, `a.each`), or a method the project defines. So every reference escapes
18
+ # unless it is in one of these positions:
19
+ #
20
+ # - the receiver of a call on the per-class {READERS} list, which neither returns, keeps nor mutates its receiver;
21
+ # - the receiver of an {ITERATORS} call or of a mutator the seams model ({.mutators_for}), with the call's value
22
+ # discarded;
23
+ # - the body's value position (its last statement, or `return a`), after which nothing in the body runs;
24
+ # - a bare read whose value is discarded.
25
+ #
26
+ # A listed name the project defines anywhere (a reopen, a prepended or included module, a refinement, any
27
+ # class at all) escapes all the same ({.project_defines?}). Every argument position escapes, and so does
28
+ # any reference inside a block or lambda, a write whose value is used (`a = b = …`), and every local of a
29
+ # body that calls `binding`, `eval`, `local_variable_set`, `instance_variable_set` or a string-form
30
+ # `instance_eval` / `class_eval` / `module_eval`.
31
+ # The scan is flow insensitive and runs once per body, so an escape anywhere drops the fact for the whole body:
32
+ # the conservative direction, since without the fact the seams answer as before. A nested `def` or class body is
33
+ # scanned too, though its locals are its own; a same-named local there can only over-report an escape.
34
+ module ConstructionEscape
35
+ CLASSES = %w[Array Hash Set].freeze
36
+
37
+ # The calls that answer a value other than the receiver, keep no reference to it, and change nothing in it.
38
+ READERS = Ractor.make_shareable(
39
+ {
40
+ "Array" => %i[
41
+ size length count empty? any? none? all? include? first last [] dig fetch index find_index sum min max
42
+ join dup map select filter reject sort sort_by uniq compact flatten reverse take drop to_h to_set ==
43
+ ].to_set,
44
+ "Hash" => %i[
45
+ size length count empty? any? include? key? has_key? member? value? has_value? [] dig fetch keys values
46
+ dup map select filter reject to_a ==
47
+ ].to_set,
48
+ "Set" => %i[size length count empty? any? include? member? first dup map select filter reject to_a ==].to_set
49
+ }
50
+ )
51
+
52
+ # The calls that answer the receiver itself and yield only its members: safe with their value discarded.
53
+ ITERATORS = Ractor.make_shareable(
54
+ {
55
+ "Array" => %i[each each_with_index each_index reverse_each].to_set,
56
+ "Hash" => %i[each each_pair each_key each_value each_with_index].to_set,
57
+ "Set" => %i[each].to_set
58
+ }
59
+ )
60
+
61
+ # Calls that read or rebind the frame's locals by name: one anywhere in a body escapes every local in it.
62
+ FRAME_CALLS = %i[binding eval local_variable_set instance_variable_set].to_set.freeze
63
+ STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].to_set.freeze
64
+
65
+ CLOSURES = [Prism::BlockNode, Prism::LambdaNode].freeze
66
+ LOCAL_REFERENCES = [
67
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
68
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
69
+ ].freeze
70
+ LOCAL_WRITES = [
71
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
72
+ Prism::LocalVariableAndWriteNode
73
+ ].freeze
74
+ ALL = :all
75
+ EMPTY_CALLS = Set.new.freeze
76
+ # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so a long-lived process never serves a verdict
77
+ # from a body an earlier run parsed.
78
+ CACHE_KEY = RunScopedSlots.register(:__rigor_construction_escape)
79
+ private_constant :CLOSURES, :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :EMPTY_CALLS, :CACHE_KEY,
80
+ :STRING_EVAL_CALLS
81
+
82
+ # One body's answer: per name, ALL or the classes it escapes as, and the calls the scan admitted on it.
83
+ Result = Data.define(:escapes, :calls)
84
+ private_constant :Result
85
+
86
+ module_function
87
+
88
+ # True when the local `name`, bound to a carrier of `class_name`, may escape somewhere in `body`, or when
89
+ # `body` is unknown. `scope` answers whether the project defines one of the admitted calls on the class
90
+ # ({.project_defines?}).
91
+ def escaped?(body, name, class_name, scope)
92
+ return true if body.nil? || class_name.nil?
93
+
94
+ table = (Thread.current[CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
95
+ result = table[body] ||= scan(body)
96
+ return true if result.equal?(ALL)
97
+
98
+ reason = result.escapes[name]
99
+ return true if !reason.nil? && (reason.equal?(ALL) || reason.include?(class_name))
100
+
101
+ result.calls.fetch(name, EMPTY_CALLS).any? { |call| project_defines?(scope, class_name, call) }
102
+ end
103
+
104
+ # Whether the project defines a method named `call` anywhere ({Scope#project_method_name?}): on any class or
105
+ # module, instance or singleton side, in a `refine` block, in any file. An admitted call of such a name escapes
106
+ # the local whatever the receiver's class, because a prepended or included module, a refinement or a reopen can
107
+ # put that name on Array, Hash or Set, and telling which owner reaches the receiver is not worth the precision
108
+ # it buys: without the fact the seams answer as before. When no def is found the def-node lookup on the class
109
+ # itself still runs, so an incremental run records the missing `Array#size` and a later reopen re-analyses
110
+ # this file.
111
+ def project_defines?(scope, class_name, call)
112
+ scope.project_method_name?(call) || !scope.user_def_for(class_name, call).nil?
113
+ end
114
+
115
+ # The receiver mutators the seams model for `class_name` ({MutationWidening}, {HashLookupMutation},
116
+ # {SetMutation}).
117
+ def mutators_for(class_name)
118
+ case class_name
119
+ when "Array" then MutationWidening::ARRAY_MUTATORS
120
+ when "Hash" then MutationWidening::HASH_MUTATORS | HashLookupMutation::MUTATORS
121
+ else SetMutation::MUTATORS
122
+ end
123
+ end
124
+
125
+ def scan(body)
126
+ escapes = {}
127
+ calls = {}
128
+ Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
129
+ return ALL if frame_call?(node)
130
+ next unless LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
131
+
132
+ record(escapes, calls, node, ancestors)
133
+ end
134
+ Result.new(escapes: escapes.freeze, calls: calls.freeze)
135
+ end
136
+
137
+ def record(escapes, calls, node, ancestors)
138
+ name = node.name
139
+ call = receiver_call(node, ancestors)
140
+ unsafe =
141
+ if call
142
+ unsafe_classes(call, ancestors).tap { |u| (calls[name] ||= Set.new) << call.name if u.size < CLASSES.size }
143
+ else
144
+ escape_reference?(node, ancestors) ? CLASSES : []
145
+ end
146
+ return if unsafe.empty? || escapes[name].equal?(ALL)
147
+
148
+ escapes[name] = unsafe.size == CLASSES.size ? ALL : (escapes[name] || Set.new).merge(unsafe)
149
+ end
150
+
151
+ def frame_call?(node)
152
+ return false unless node.is_a?(Prism::CallNode)
153
+
154
+ FRAME_CALLS.include?(node.name) || (STRING_EVAL_CALLS.include?(node.name) && !node.arguments.nil?)
155
+ end
156
+
157
+ # The call a read outside any closure is the receiver of, or nil.
158
+ def receiver_call(node, ancestors)
159
+ return nil unless node.is_a?(Prism::LocalVariableReadNode) && !in_closure?(ancestors)
160
+
161
+ parent = ancestors.last
162
+ parent if parent.is_a?(Prism::CallNode) && parent.receiver.equal?(node)
163
+ end
164
+
165
+ # The classes for which `call` on the local may hand it on.
166
+ def unsafe_classes(call, ancestors)
167
+ discarded = !value_used?(ancestors, ancestors.size - 2, call)
168
+ CLASSES.reject do |class_name|
169
+ READERS.fetch(class_name).include?(call.name) ||
170
+ (discarded && (ITERATORS.fetch(class_name).include?(call.name) ||
171
+ mutators_for(class_name).include?(call.name)))
172
+ end
173
+ end
174
+
175
+ def in_closure?(ancestors)
176
+ ancestors.any? { |a| CLOSURES.any? { |k| a.is_a?(k) } }
177
+ end
178
+
179
+ # Whether a reference that is no call's receiver may hand the object on.
180
+ def escape_reference?(node, ancestors)
181
+ return true if in_closure?(ancestors)
182
+ return value_used?(ancestors, ancestors.size - 1, node) if LOCAL_WRITES.any? { |k| node.is_a?(k) }
183
+ return false unless node.is_a?(Prism::LocalVariableReadNode)
184
+
185
+ parent = ancestors.last
186
+ return false if parent.is_a?(Prism::ArgumentsNode) && ancestors[-2].is_a?(Prism::ReturnNode)
187
+
188
+ !(parent.is_a?(Prism::StatementsNode) && !value_used?(ancestors, ancestors.size - 1, node))
189
+ end
190
+
191
+ # Whether the value of `node`, whose parent is `ancestors[index]`, reaches anything. The body's own value (its
192
+ # last statement) is the return position, after which nothing in the body runs, so it reads as unused.
193
+ def value_used?(ancestors, index, node)
194
+ while index >= 0
195
+ parent = ancestors[index]
196
+ case parent
197
+ when Prism::StatementsNode
198
+ return false unless parent.body.last.equal?(node)
199
+ when Prism::IfNode, Prism::UnlessNode, Prism::CaseNode, Prism::CaseMatchNode
200
+ return true if parent.predicate.equal?(node)
201
+ when Prism::WhenNode, Prism::InNode
202
+ return true unless parent.statements.equal?(node)
203
+ when Prism::WhileNode, Prism::UntilNode, Prism::EnsureNode
204
+ return !parent.statements.equal?(node)
205
+ when Prism::ElseNode, Prism::BeginNode, Prism::ParenthesesNode, Prism::ProgramNode, Prism::RescueNode
206
+ nil
207
+ else
208
+ return true
209
+ end
210
+ node = parent
211
+ index -= 1
212
+ end
213
+ false
214
+ end
215
+ end
216
+ end
217
+ end
@@ -235,7 +235,7 @@ module Rigor
235
235
  # A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
236
236
  # each collector alone (a pair in which one declines), the run without each collector (a larger run in
237
237
  # which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
238
- # them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
238
+ # them. `kind` names the table; `key` is the collector, or the rule's Symbol for a split.
239
239
  def subset(kind, collector = nil)
240
240
  table = (@subsets[kind] ||= {}.compare_by_identity)
241
241
  key = collector || kind
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../type"
5
+ require_relative "../source/node_walker"
6
+ require_relative "mutation_rejoin"
7
+ require_relative "receiver_alias"
8
+ require_relative "set_mutation"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # A loop hands its collection's own elements to its body (issue #2262), so a body that mutates one in place
13
+ # changes a shape that the collection's binding still carries one level down. This module is the two halves of
14
+ # dropping that shape: whether the body mutates an element, and the type with every shape dropped at every depth.
15
+ module DeepErasure
16
+ module_function
17
+
18
+ # {MutationWidening.widen_tuple} / {MutationWidening.widen_hash_shape} applied at every depth: each `Tuple` /
19
+ # `HashShape` the type holds, as a union member or a type argument, drops to its nominal, so `[[1]]` reads
20
+ # `Array[Array[Integer]]`. A loop's element is the collection's own object, so a body that mutates it in place
21
+ # (`x << 2`) changes a shape one level down that the collection's binding still carries.
22
+ def widen(type)
23
+ case type
24
+ when Type::Tuple then widen_args(MutationWidening.widen_tuple(type))
25
+ when Type::HashShape then widen_args(MutationWidening.widen_hash_shape(type))
26
+ when Type::Nominal then widen_args(type)
27
+ when Type::Difference then widen(type.base)
28
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| widen(member) })
29
+ else Type::Combinator.widen_value_pinned(type)
30
+ end
31
+ end
32
+
33
+ def widen_args(nominal)
34
+ return nominal unless nominal.is_a?(Type::Nominal) && !nominal.type_args.empty?
35
+
36
+ Type::Combinator.nominal_of(nominal.class_name, type_args: nominal.type_args.map { |arg| widen(arg) })
37
+ end
38
+
39
+ # The parameter nodes whose names a block (or a `for` index) binds to the collection's elements.
40
+ ELEMENT_PARAMETER_NODES = [
41
+ Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
42
+ Prism::KeywordRestParameterNode, Prism::LocalVariableTargetNode
43
+ ].freeze
44
+ ELEMENT_WRITE_NODES = [
45
+ Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
46
+ Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
47
+ Prism::IndexTargetNode, Prism::CallTargetNode
48
+ ].freeze
49
+ private_constant :ELEMENT_PARAMETER_NODES, :ELEMENT_WRITE_NODES
50
+
51
+ # True when `body` mutates in place a value that a name in `names` (a loop's element) holds: a mutator
52
+ # (`MutationWidening::SHAPE_MUTATORS`) called on a read or an index chain rooted at it (`x << 1`,
53
+ # `x[0].push(1)`, `x[0] = 1`), or an operator write on one (`x[0] += 1`). Every name a block or a
54
+ # `for` inside the body binds counts as an element too (`x.each { |y| y << 1 }`, `for c in x; c << 1; end`),
55
+ # and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
56
+ # the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
57
+ # an element passed to a call (`add(x)`), which the straight-line widening does not either.
58
+ #
59
+ # A Set-only mutator (`add`, `merge`, `subtract`, … — {SetMutation::MUTATORS} less `SHAPE_MUTATORS`) counts
60
+ # only when `set_held` is given and answers true (issue #2363). The names are read without their receiver,
61
+ # and `merge` on a Hash element returns a new Hash, so the caller answers from the collection's type whether
62
+ # an element could be a Set at all ({holds_set?}). The block runs at the first such site, at most once.
63
+ def element_mutation?(body, names, &set_held)
64
+ return false if body.nil?
65
+
66
+ names |= nested_parameter_names(body)
67
+ Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names, set_held) }
68
+ end
69
+
70
+ # True when `type` holds a `Set` at some depth: as itself, a union member, a `Tuple` element, a `HashShape`
71
+ # value or a type argument. A collection whose type holds none has no element a Set-only mutator can be
72
+ # mutating, so a `merge` on one of its elements is `Hash#merge`, which returns a new Hash.
73
+ def holds_set?(type)
74
+ case type
75
+ when Type::Constant then SetMutation.constant?(type)
76
+ when Type::Nominal then type.class_name == "Set" || type.type_args.any? { |arg| holds_set?(arg) }
77
+ when Type::Tuple then type.elements.any? { |element| holds_set?(element) }
78
+ when Type::HashShape then type.pairs.each_value.any? { |value| holds_set?(value) }
79
+ when Type::Union then type.members.any? { |member| holds_set?(member) }
80
+ when Type::Difference then holds_set?(type.base)
81
+ else false
82
+ end
83
+ end
84
+
85
+ # The names `parameters` (a block's parameter list, or a `for` index) binds.
86
+ def parameter_names(parameters)
87
+ return [] if parameters.nil?
88
+
89
+ Source::NodeWalker.each(parameters).filter_map do |node|
90
+ node.name if ELEMENT_PARAMETER_NODES.include?(node.class)
91
+ end
92
+ end
93
+
94
+ def nested_parameter_names(body)
95
+ Source::NodeWalker.each(body).flat_map do |node|
96
+ case node
97
+ when Prism::BlockNode then parameter_names(node.parameters)
98
+ when Prism::ForNode then parameter_names(node.index)
99
+ else []
100
+ end
101
+ end
102
+ end
103
+
104
+ def element_mutation_site?(node, names, set_held)
105
+ if node.is_a?(Prism::CallNode)
106
+ return false unless element_rooted?(node.receiver, names)
107
+
108
+ MutationWidening::SHAPE_MUTATORS.include?(node.name) ||
109
+ (!set_held.nil? && SetMutation::MUTATORS.include?(node.name) && set_held.call)
110
+ else
111
+ ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
112
+ end
113
+ end
114
+
115
+ def element_rooted?(node, names)
116
+ node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
117
+ node = node.body.body.last while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
118
+ case node
119
+ when Prism::LocalVariableReadNode then names.include?(node.name) || /\A_[1-9]\z/.match?(node.name)
120
+ when Prism::ItLocalVariableReadNode then true
121
+ else false
122
+ end
123
+ end
124
+
125
+ # A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
126
+ # place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
127
+ # (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
128
+ # value does ({StatementEvaluator#for_collection_after}). A receiver that is a call exposing a variable's
129
+ # elements (`h.values`, `a.dup`, `a.each_with_index`) names that variable ({ReceiverAlias.element_sources}).
130
+ def widen_element_aliases(receiver, body, names, scope)
131
+ return scope unless element_mutation?(body, names) { receiver_holds_set?(receiver, scope) }
132
+
133
+ widen_receiver(receiver, scope)
134
+ end
135
+
136
+ def receiver_holds_set?(receiver, scope)
137
+ ReceiverAlias.element_sources(receiver).any? do |read|
138
+ getter, = MutationWidening::ALIAS_ACCESSORS[read.class]
139
+ getter && holds_set?(scope.public_send(getter, ReceiverAlias.read_name(read)))
140
+ end
141
+ end
142
+
143
+ def widen_receiver(receiver, scope)
144
+ ReceiverAlias.element_sources(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
145
+ end
146
+
147
+ # `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
148
+ def symbol_proc_mutation?(block)
149
+ block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode) &&
150
+ MutationWidening::SHAPE_MUTATORS.include?(block.expression.unescaped.to_sym)
151
+ end
152
+
153
+ def widen_symbol_proc(call_node, scope) = widen_receiver(call_node.receiver, scope)
154
+
155
+ def widen_read(read, scope)
156
+ getter, builder = MutationWidening::ALIAS_ACCESSORS[read.class]
157
+ return scope if getter.nil?
158
+
159
+ name = ReceiverAlias.read_name(read)
160
+ current = scope.public_send(getter, name)
161
+ return scope if current.nil?
162
+
163
+ widened = widen(current)
164
+ return scope if widened == current
165
+
166
+ MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -8,7 +8,7 @@ module Rigor
8
8
  # file's methods: the file was never parsed this run, so there is no live node to store. A handle records
9
9
  # the four things needed to resolve, fingerprint, or scope it without the node:
10
10
  #
11
- # - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
11
+ # - `path` + `node_id` — the resolution key. The per-run parse memo ({DefNodeResolver.resolve}) parses the
12
12
  # file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
13
13
  # repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
14
14
  # from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
@@ -104,7 +104,7 @@ module Rigor
104
104
  # is recorded like any other: it is the bundle saying the def is written at the top level, and dropping
105
105
  # it would leave the warm path peeling the caller's namespace where a cold run resolves at the top
106
106
  # level. Only `nil` — a bundle that recorded nothing for the def — is skipped, and
107
- # `Cache::IncrementalSnapshot::SCHEMA` 17 is what keeps a pre-#716 blob (where `nil` MEANT top level)
107
+ # the `SCHEMA` bump to 17 (`Cache::IncrementalSnapshot`) keeps a pre-#716 blob, where `nil` MEANT top level,
108
108
  # from being read as one.
109
109
  def self.record_nesting(memo, node, nesting)
110
110
  return if node.nil? || nesting.nil?