rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "block_run_count"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -12,10 +13,12 @@ module Rigor
12
13
  # counted as running the block once, and a `return` in the block that only a later run reaches (`each { return
13
14
  # false if (seen += 1) > 3 }`) never joined the method's return type.
14
15
  #
15
- # Only the later-run `return` pass asks this (`StatementEvaluator#collect_repeated_block_returns`); the block's
16
- # entry scopes and the ADR-56 write-back keep {BlockRepetition}'s rule. That pass only widens the method's
17
- # return type, so the rule here errs toward "repeats". The callee repeats when its body, outside a nested `def`
18
- # (a `yield` in a class body is a syntax error inside a method):
16
+ # Only the later-run `return` pass asks {.may_run_block_repeatedly?}
17
+ # (`StatementEvaluator#collect_repeated_block_returns`); the block's entry scopes and the ADR-56 write-back keep
18
+ # {BlockRepetition}'s rule. That pass only widens the method's return type, so the rule here errs toward
19
+ # "repeats". The expression-position block-value pass asks {.block_value_may_repeat?} instead, whose rule
20
+ # ({BlockRunCount}) errs the other way. The callee repeats when its body, outside a nested `def` (a `yield` in a
21
+ # class body is a syntax error inside a method):
19
22
  #
20
23
  # - holds two or more sites that run the block (`yield`, or `call` / `yield` / `===` / `[]` on the `&blk`
21
24
  # parameter), or one inside a loop, a block or a lambda, which may run it many times; or
@@ -41,26 +44,17 @@ module Rigor
41
44
  false
42
45
  end
43
46
 
44
- # Issue #2301 — whether the callee hands the block it is called with on to another call (`g(x, &)`,
45
- # `g(x, &blk)`), outside a nested `def`. The block-return pass types such a forward's target `yield` as the
46
- # caller's block ({ExpressionTyper#forwarded_block_return_type}), and the target may run it any number of
47
- # times, so the caller's block is typed as one that may repeat. Answers false where the callee does not resolve.
48
- def hands_block_on?(call_node, receiver_type, scope)
47
+ # Issues #2301 and #2303 — whether the callee may run the block more than once on one path, for the
48
+ # expression-position block-value pass ({BlockRunCount}). Answers false where the callee does not resolve.
49
+ def block_value_may_repeat?(call_node, receiver_type, scope)
49
50
  def_node = callee_def(call_node, receiver_type, scope)
50
51
  return false unless def_node.is_a?(Prism::DefNode) && def_node.body
51
52
 
52
- hand_off_in?(def_node.body, def_node.parameters&.block&.name)
53
+ BlockRunCount.may_repeat?(def_node, scope)
53
54
  rescue StandardError
54
55
  false
55
56
  end
56
57
 
57
- def hand_off_in?(node, block_name)
58
- return false if node.is_a?(Prism::DefNode)
59
- return true if node.is_a?(Prism::BlockArgumentNode) && forwards_block?(node, block_name)
60
-
61
- node.compact_child_nodes.any? { |child| hand_off_in?(child, block_name) }
62
- end
63
-
64
58
  # The `def` an instance call on a project class, a singleton call on one, or an implicit-self call to a
65
59
  # top-level method resolves to.
66
60
  def callee_def(call_node, receiver_type, scope)
@@ -85,6 +85,26 @@ module Rigor
85
85
  candidates(first, depth + 1) + candidates(second, depth + 1)
86
86
  end
87
87
 
88
+ # Calls whose result hands its caller the receiver's own element objects (issue #2287): `h.values` the hash's
89
+ # values, `a.dup` / `a.clone` a shallow copy sharing them, and the blockless enumerator spellings (`a.each`,
90
+ # `a.map`, `a.each_with_index`, `e.with_index`) the elements themselves.
91
+ ELEMENT_EXPOSING_CALLS = %i[values dup clone each map each_with_index with_index].to_set.freeze
92
+ private_constant :ELEMENT_EXPOSING_CALLS
93
+
94
+ # The variables whose elements a loop over `receiver` is handed: {.candidates} of `receiver` once every
95
+ # blockless {ELEMENT_EXPOSING_CALLS} call is peeled off, so `h.values.each { |v| v << 2 }` and
96
+ # `a.each_with_index.map { |x, i| x << i }` name `h` and `a`. Only a loop's element widening reads it; an
97
+ # in-place mutation OF such a result (`h.values << 1`, `a.dup << 1`) changes a new object, not the variable.
98
+ def element_sources(receiver)
99
+ depth = 0
100
+ while receiver.is_a?(Prism::CallNode) && receiver.block.nil? && ELEMENT_EXPOSING_CALLS.include?(receiver.name)
101
+ return [] if (depth += 1) > WALK_DEPTH_CAP
102
+
103
+ receiver = receiver.receiver
104
+ end
105
+ candidates(receiver)
106
+ end
107
+
88
108
  # The variable reads an in-place mutation of `receiver` changes: {.candidates}' locals and instance
89
109
  # variables, or the class variable or global the receiver reads or writes directly, parenthesised or not
90
110
  # (`(@@c ||= []) << x` mutates `@@c`, as `(buf ||= []) << x` mutates `buf`). A class variable or global counts
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../run_scoped_slots"
4
+
3
5
  module Rigor
4
6
  module Inference
5
7
  module RefinedDispatch
@@ -16,7 +18,7 @@ module Rigor
16
18
  # the name itself, and no constant write of it. Any other shape answers false, as does a class a
17
19
  # `K = Class.new(...)` write names.
18
20
  module ConstructedClass
19
- MEMO_KEY = :__rigor_constructed_class__
21
+ MEMO_KEY = RunScopedSlots.register(:__rigor_constructed_class__)
20
22
  EMPTY = [].freeze
21
23
  private_constant :MEMO_KEY, :EMPTY
22
24
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../builtins/cruby_definers"
4
4
  require_relative "constructed_class"
5
+ require_relative "../../run_scoped_slots"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -317,7 +318,7 @@ module Rigor
317
318
  all.include?(name) ? all - [name] : all
318
319
  end
319
320
 
320
- SUBCLASS_MEMO_KEY = :__rigor_own_method_subclasses__
321
+ SUBCLASS_MEMO_KEY = RunScopedSlots.register(:__rigor_own_method_subclasses__)
321
322
  private_constant :SUBCLASS_MEMO_KEY
322
323
 
323
324
  # One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../run_scoped_slots"
6
+ require_relative "closure_escape_analyzer"
7
+ require_relative "construction_escape"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2608, review rounds 1 and 2 of #2621 — which locals of a body another holder may write, so a remover on
12
+ # them is not narrowed ({RemoverNarrowing}): a narrowing is a fact about the object, and a write through any other
13
+ # holder after the call would outlive it. One flow-insensitive scan per frame body; a local is a holder's when:
14
+ #
15
+ # - a closure that may run later references it: a lambda, or a block passed to a call that is not a catalogued
16
+ # iterator or `tap` / `then` / `yield_self` (`proc { l << 1 }`), created before or after the remover;
17
+ # - it is read anywhere inside the value of any write (local, instance, class, global or constant variable, an
18
+ # index or attribute write, an operator or `||=` / `&&=` write, a multiple assignment) or inside the subject of a
19
+ # `case`/`in`, `=>` or `in` pattern: `al = c ? l : []`, `al = l.push(1)`, `@k = l`, `case l; in al`. The local
20
+ # such a write or pattern binds is a holder too (both names of a copy), unless the write's value is a fresh
21
+ # object: an Array or Hash literal, or `new` / `[]` on a bare or top-level `Array`, `Hash` or `Set`
22
+ # (`a = Array.new(n, "a")`);
23
+ # - a method's parameter, when its body has a zero-argument `super`, which passes every parameter on;
24
+ # - it is read inside an Array element, a Hash key or value, a splat, or an argument of any call, `yield` or
25
+ # `super` (positional, keyword, splat or block-pass), unless the read is the receiver of a call that answers
26
+ # a value other than the receiver and keeps no reference to it ({PURE_READERS}: #2555's per-class
27
+ # `ConstructionEscape::READERS`, less the conversions that return self on another class): `puts(l.size)`,
28
+ # `dump_type(l.dup)` and `dump_type(l.first)` hold nothing, `Keep.new(l)` and `foo(l.push(1))` do;
29
+ # - it receives a reflective call (`l.send(...)`, `l.instance_eval { }`, `l.tap { }`);
30
+ # - and every local, when the body calls `binding`, `eval`, `local_variable_get` or `local_variable_set` on any
31
+ # receiver (`-> {}.binding`), or a receiverless `instance_variable_set`.
32
+ #
33
+ # A receiver position, a `return` and a body's last expression hand nothing on.
34
+ module RemoverHolderScan
35
+ REFLECTIVE_CALLS = %i[
36
+ send public_send __send__ instance_eval instance_exec tap then yield_self itself method
37
+ define_singleton_method
38
+ ].to_set.freeze
39
+ # A conversion one class lists as a reader returns the receiver itself on another (`Array#to_a`, `Hash#to_h`,
40
+ # `Set#to_set`), so the union of the per-class lists leaves them out.
41
+ SELF_CONVERSIONS = %i[to_a to_h to_set].freeze
42
+ PURE_READERS = (ConstructionEscape::READERS.values.reduce(:|) - SELF_CONVERSIONS).freeze
43
+ IMMEDIATE_BLOCK_CALLS = %i[tap then yield_self].to_set.freeze
44
+ FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
45
+ # Frame calls that reach the locals whatever their receiver (`pr.binding`, `Kernel.eval`).
46
+ ANY_RECEIVER_FRAME_CALLS = %i[binding eval local_variable_get local_variable_set].to_set.freeze
47
+ NON_WRITER_OPERATORS = %i[== != <= >= ===].to_set.freeze
48
+ CORE_COLLECTIONS = %i[Array Hash Set].to_set.freeze
49
+ LOCAL_REFERENCES = [
50
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
51
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
52
+ ].freeze
53
+ LOCAL_WRITES = [
54
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
55
+ Prism::LocalVariableAndWriteNode
56
+ ].freeze
57
+ ALL = :all
58
+ # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so no verdict outlives the body it read.
59
+ UNSAFE_CACHE_KEY = RunScopedSlots.register(:__rigor_remover_narrowing_unsafe)
60
+ private_constant :REFLECTIVE_CALLS, :SELF_CONVERSIONS, :PURE_READERS, :IMMEDIATE_BLOCK_CALLS, :FRAME_CALLS,
61
+ :ANY_RECEIVER_FRAME_CALLS, :NON_WRITER_OPERATORS, :CORE_COLLECTIONS,
62
+ :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :UNSAFE_CACHE_KEY
63
+
64
+ module_function
65
+
66
+ # Whether the local `name` may be written through another holder somewhere in the body `scope` runs. A scope
67
+ # with no known body answers true.
68
+ def unsafe_receiver?(scope, name)
69
+ frame = scope.match_frame
70
+ body = frame&.body
71
+ return true if body.nil?
72
+
73
+ table = (Thread.current[UNSAFE_CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
74
+ names = table[body] ||= unsafe_names(body, frame.parameters)
75
+ names.equal?(ALL) || names.include?(name.to_sym)
76
+ end
77
+
78
+ def unsafe_names(body, parameters = nil)
79
+ names = Set.new
80
+ catch(ALL) do
81
+ collect_unsafe(body, false, names)
82
+ parameter_names(parameters, names) if forwards_parameters?(body)
83
+ return names.freeze
84
+ end
85
+ ALL
86
+ end
87
+
88
+ def collect_unsafe(node, in_closure, names)
89
+ in_closure ||= node.is_a?(Prism::LambdaNode)
90
+ names << node.name if in_closure && LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
91
+ record_node(node, names)
92
+
93
+ node.compact_child_nodes.each do |child|
94
+ child_in_closure = in_closure || (child.is_a?(Prism::BlockNode) && deferred_block?(node))
95
+ collect_unsafe(child, child_in_closure, names)
96
+ end
97
+ end
98
+
99
+ def record_node(node, names) # rubocop:disable Metrics/CyclomaticComplexity
100
+ case node
101
+ when Prism::CallNode then record_call(node, names)
102
+ when Prism::YieldNode, Prism::SuperNode then node.arguments&.arguments&.each { |a| record_held(a, names) }
103
+ when Prism::ArrayNode then node.elements.each { |element| record_held(element, names) }
104
+ when Prism::AssocNode then [node.key, node.value].each { |part| record_held(part, names) }
105
+ when Prism::SplatNode, Prism::BlockArgumentNode then record_held(node.expression, names)
106
+ when Prism::CaseMatchNode then record_pattern(node.predicate, node.conditions, names)
107
+ when Prism::MatchRequiredNode, Prism::MatchPredicateNode then record_pattern(node.value, [node.pattern], names)
108
+ when Prism::MultiWriteNode then record_multi_write(node, names)
109
+ else record_write(node, names)
110
+ end
111
+ end
112
+
113
+ def record_call(node, names)
114
+ throw ALL if frame_call?(node)
115
+
116
+ receiver = unparenthesised(node.receiver)
117
+ names << receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && REFLECTIVE_CALLS.include?(node.name)
118
+ arguments = node.arguments&.arguments || []
119
+ if writer_call?(node) && !arguments.empty?
120
+ *keys, value = arguments
121
+ keys.each { |key| record_held(key, names) }
122
+ every_read(value, names)
123
+ else
124
+ arguments.each { |argument| record_held(argument, names) }
125
+ end
126
+ end
127
+
128
+ # A zero-argument `super` passes every parameter of the method on, implicitly. (A `...` forward passes only the
129
+ # anonymous parameters, which name no local.)
130
+ def forwards_parameters?(node)
131
+ node.is_a?(Prism::ForwardingSuperNode) || node.compact_child_nodes.any? { |child| forwards_parameters?(child) }
132
+ end
133
+
134
+ def parameter_names(node, names)
135
+ return if node.nil?
136
+
137
+ names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
138
+ node.compact_child_nodes.each { |child| parameter_names(child, names) }
139
+ end
140
+
141
+ def frame_call?(node)
142
+ ANY_RECEIVER_FRAME_CALLS.include?(node.name) || (node.receiver.nil? && FRAME_CALLS.include?(node.name))
143
+ end
144
+
145
+ def unparenthesised(node)
146
+ node = node.body.body.first while node.is_a?(Prism::ParenthesesNode) && node.body&.body&.size == 1
147
+ node
148
+ end
149
+
150
+ # An index or attribute write (`h[k] = v`, `o.x = v`, `o&.x = v`): its value is a write's value.
151
+ def writer_call?(node)
152
+ name = node.name
153
+ name.end_with?("=") && !NON_WRITER_OPERATORS.include?(name)
154
+ end
155
+
156
+ # A write's value hands on every local read anywhere inside it, and the local it binds holds what it read.
157
+ def record_write(node, names)
158
+ return unless node.class.name.end_with?("WriteNode") && node.respond_to?(:value)
159
+ return unless every_read(node.value, names)
160
+
161
+ names << node.name if LOCAL_WRITES.any? { |k| node.is_a?(k) } && !fresh_value?(node.value)
162
+ end
163
+
164
+ # `Array`, `Hash` or `Set`, bare or as a top-level path (`::Set`): a project class may define its own `self.[]` or
165
+ # `self.new` that returns its argument.
166
+ def core_collection_constant?(node)
167
+ top_level = node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)
168
+ top_level && CORE_COLLECTIONS.include?(node.name)
169
+ end
170
+
171
+ # A value that is a new object whatever it reads, so the local it binds aliases nothing.
172
+ def fresh_value?(value)
173
+ case value
174
+ when Prism::ArrayNode, Prism::HashNode then true
175
+ when Prism::CallNode then %i[new []].include?(value.name) && core_collection_constant?(value.receiver)
176
+ else false
177
+ end
178
+ end
179
+
180
+ def record_multi_write(node, names)
181
+ return unless every_read(node.value, names)
182
+
183
+ local_targets([*node.lefts, node.rest, *node.rights], names)
184
+ end
185
+
186
+ def record_pattern(subject, patterns, names)
187
+ return unless every_read(subject, names)
188
+
189
+ local_targets(patterns, names)
190
+ end
191
+
192
+ # Adds every local `node` reads, at any depth; answers whether it read any.
193
+ def every_read(node, names)
194
+ return false if node.nil?
195
+
196
+ found = node.is_a?(Prism::LocalVariableReadNode)
197
+ names << node.name if found
198
+ node.compact_child_nodes.reduce(found) { |acc, child| every_read(child, names) || acc }
199
+ end
200
+
201
+ def local_targets(nodes, names)
202
+ nodes.compact.each do |node|
203
+ names << node.name if node.is_a?(Prism::LocalVariableTargetNode)
204
+ local_targets(node.compact_child_nodes, names)
205
+ end
206
+ end
207
+
208
+ # A held position: every local read inside it but the receiver of a {PURE_READERS} call.
209
+ def record_held(node, names)
210
+ return if node.nil?
211
+ return if pure_read?(node)
212
+
213
+ names << node.name if node.is_a?(Prism::LocalVariableReadNode)
214
+ node.compact_child_nodes.each { |child| record_held(child, names) }
215
+ end
216
+
217
+ # A {PURE_READERS} call on a local hands the local on to nothing. Its own arguments are held positions of the
218
+ # call itself, which the body walk records when it reaches that call.
219
+ def pure_read?(node)
220
+ node.is_a?(Prism::CallNode) && PURE_READERS.include?(node.name) &&
221
+ node.receiver.is_a?(Prism::LocalVariableReadNode)
222
+ end
223
+
224
+ # A block that may run after its call returns: any but a catalogued iterator's, or `tap` / `then` /
225
+ # `yield_self`'s.
226
+ def deferred_block?(call_node)
227
+ return true unless call_node.is_a?(Prism::CallNode)
228
+
229
+ !(ClosureEscapeAnalyzer.iterator_name?(call_node.name) || IMMEDIATE_BLOCK_CALLS.include?(call_node.name))
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "remover_holder_scan"
7
+ # `Narrowing` is resolved at call time: requiring it here closes a load cycle through `KeyPresenceGuard`.
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2608 — a collection remover or filter narrows its receiver's element (or Hash key / value) type by what
12
+ # Ruby guarantees it removed, where that is provable from the call alone:
13
+ #
14
+ # - `Array#compact!` / `Hash#compact!` drop `nil` from the element / value;
15
+ # - `Array#delete(nil)` / `Set#delete(nil)` / `Set#delete?(nil)` drop `nil` from the element, and `Hash#delete(nil)`
16
+ # drops it from the key, given a literal `nil`;
17
+ # - `reject!` / `delete_if` keep the elements the block answers falsey for, and `select!` / `filter!` / `keep_if`
18
+ # the ones it answers truthy for. The block's one expression is read by the engine's own predicate narrowing
19
+ # ({Narrowing.predicate_scopes}) with its parameter bound to the element (the Hash `|k, v|` form binds both), and
20
+ # the surviving element is that parameter's binding on the matching edge. `&:nil?` reads as `{ |x| x.nil? }`;
21
+ # - a closed `HashShape` loses the key a literal `delete(:k)` names.
22
+ #
23
+ # Everything else (`pop`, `shift`, `slice!`, `subtract`, `delete` of a non-literal, a block whose predicate the
24
+ # narrowing does not read) answers `nil`: which members remain is not provable, and the caller keeps its reading.
25
+ #
26
+ # Only a receiver that is itself a local read narrows, and only when no other holder in its body may write the
27
+ # object ({RemoverHolderScan}): the narrowing is a fact about the object. A receiver selecting among bindings
28
+ # (`(f ? a : b).compact!`) mutates one of them, and narrowing every candidate would drop a member from the one the
29
+ # call never touched. An instance variable never narrows: any method, or any object that holds it, can write it.
30
+ module RemoverNarrowing
31
+ NIL_REJECTORS = %i[compact!].to_set.freeze
32
+ NIL_LITERAL_DELETERS = %i[delete delete?].to_set.freeze
33
+ FALSEY_KEEPERS = %i[reject! delete_if].to_set.freeze
34
+ TRUTHY_KEEPERS = %i[select! filter! keep_if].to_set.freeze
35
+ CANDIDATES = (NIL_REJECTORS | NIL_LITERAL_DELETERS | FALSEY_KEEPERS | TRUTHY_KEEPERS).freeze
36
+ NARROWING_RECEIVERS = [Prism::LocalVariableReadNode].freeze
37
+ CARRIERS = %w[Array Hash Set].freeze
38
+ # Nodes that make a one-expression block's value something other than its predicate, or rebind a parameter.
39
+ OPAQUE_BLOCK_NODES = [
40
+ Prism::NextNode, Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode, Prism::YieldNode,
41
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
42
+ Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
43
+ ].freeze
44
+ private_constant :NIL_REJECTORS, :NIL_LITERAL_DELETERS, :FALSEY_KEEPERS, :TRUTHY_KEEPERS, :NARROWING_RECEIVERS,
45
+ :CARRIERS, :OPAQUE_BLOCK_NODES
46
+
47
+ module_function
48
+
49
+ # Whether `call_node` may narrow its receiver at all: a cheap syntactic gate for {.narrow}.
50
+ def candidate?(call_node)
51
+ CANDIDATES.include?(call_node.name) && NARROWING_RECEIVERS.any? { |klass| call_node.receiver.is_a?(klass) }
52
+ end
53
+
54
+ # The closed `HashShape` `shape` without the key a literal `delete(:k)` names, or `nil` when the call is no such
55
+ # delete or the shape is open (an unseen key may be the one deleted, and the opened default reads it back).
56
+ def narrow_shape(shape, call_node)
57
+ return nil unless shape.is_a?(Type::HashShape) && shape.closed? && call_node.name == :delete
58
+
59
+ key = literal_key(single_argument(call_node))
60
+ return nil if key.equal?(NO_KEY)
61
+ return shape unless shape.pairs.key?(key)
62
+ return nil if shape.read_only_key?(key)
63
+
64
+ Type::Combinator.hash_shape_of(
65
+ shape.pairs.except(key),
66
+ required_keys: shape.required_keys - [key], optional_keys: shape.optional_keys - [key],
67
+ read_only_keys: shape.read_only_keys, extra_keys: :closed
68
+ )
69
+ end
70
+
71
+ # `type` with each Array / Hash / Set member narrowed by what `call_node` removes, member-wise through a union.
72
+ # `nil` when the call narrows no member (it is no exact remover for the member's class, or its block predicate
73
+ # says nothing about the element). A member the call answers for without change (`compact!` on `Array[Integer]`)
74
+ # still counts as answered: the caller then keeps the precise reading rather than a floored one.
75
+ def narrow(type, call_node, scope)
76
+ answered = false
77
+ members = members_of(type).map do |member|
78
+ narrowed = narrow_member(member, call_node, scope)
79
+ next member if narrowed.nil?
80
+
81
+ answered = true
82
+ narrowed
83
+ end
84
+ answered ? Type::Combinator.union(*members) : nil
85
+ end
86
+
87
+ def members_of(type)
88
+ type.is_a?(Type::Union) ? type.members : [type]
89
+ end
90
+
91
+ def narrow_member(member, call_node, scope)
92
+ return nil unless member.is_a?(Type::Nominal) && CARRIERS.include?(member.class_name)
93
+
94
+ args = member.type_args
95
+ case member.class_name
96
+ when "Hash" then args.size == 2 ? narrow_hash(member, call_node, scope) : nil
97
+ else args.size == 1 ? narrow_elements(member, call_node, scope) : nil
98
+ end
99
+ end
100
+
101
+ def narrow_elements(member, call_node, scope)
102
+ element = member.type_args.first
103
+ narrowed =
104
+ case call_node.name
105
+ when :compact! then Narrowing.narrow_non_nil(element)
106
+ when :delete, :delete? then nil_argument?(call_node) ? Narrowing.narrow_non_nil(element) : nil
107
+ else block_narrowed(call_node, scope, [element])&.first
108
+ end
109
+ usable?(narrowed) ? Type::Combinator.nominal_of(member.class_name, type_args: [narrowed]) : nil
110
+ end
111
+
112
+ def narrow_hash(member, call_node, scope)
113
+ key, value = member.type_args
114
+ narrowed =
115
+ case call_node.name
116
+ when :compact! then [key, Narrowing.narrow_non_nil(value)]
117
+ when :delete then nil_argument?(call_node) ? [Narrowing.narrow_non_nil(key), value] : nil
118
+ when :delete? then nil
119
+ else block_narrowed(call_node, scope, [key, value])
120
+ end
121
+ narrowed&.all? { |arg| usable?(arg) } ? Type::Combinator.nominal_of("Hash", type_args: narrowed) : nil
122
+ end
123
+
124
+ # A parameter narrowed to `bot` (`select! { |x| x.nil? }` on `Array[Integer]`) says the carrier is empty; that
125
+ # is left to the caller's reading rather than spelled as `Array[bot]`.
126
+ def usable?(type)
127
+ !type.nil? && !type.is_a?(Type::Bot)
128
+ end
129
+
130
+ def nil_argument?(call_node)
131
+ single_argument(call_node).is_a?(Prism::NilNode)
132
+ end
133
+
134
+ def single_argument(call_node)
135
+ arguments = call_node.arguments&.arguments
136
+ arguments&.size == 1 ? arguments.first : nil
137
+ end
138
+
139
+ NO_KEY = Object.new.freeze
140
+ private_constant :NO_KEY
141
+
142
+ def literal_key(node)
143
+ case node
144
+ when Prism::SymbolNode then node.unescaped.to_sym
145
+ else NO_KEY
146
+ end
147
+ end
148
+
149
+ # The block parameters' types on the edge that keeps an element, or `nil` when the call has no block the
150
+ # narrowing reads: one expression, no jump or local write, explicit required parameters matching the carrier
151
+ # (`|x|` for an element, `|k, v|` for a Hash pair), and a predicate that narrows one of them on some edge.
152
+ def block_narrowed(call_node, scope, types)
153
+ keep = keep_edge(call_node.name)
154
+ return nil if keep.nil?
155
+
156
+ block = call_node.block
157
+ return symbol_nil_narrowed(block, keep, types) if block.is_a?(Prism::BlockArgumentNode)
158
+
159
+ names = block_parameter_names(block, types.size)
160
+ predicate = names && block_predicate(block)
161
+ return nil if predicate.nil?
162
+
163
+ predicate_narrowed(predicate, scope, names, types, keep)
164
+ end
165
+
166
+ def keep_edge(method_name)
167
+ if FALSEY_KEEPERS.include?(method_name) then :falsey
168
+ elsif TRUTHY_KEEPERS.include?(method_name) then :truthy
169
+ end
170
+ end
171
+
172
+ def symbol_nil_narrowed(block, keep, types)
173
+ symbol = block.expression
174
+ return nil unless types.size == 1 && symbol.is_a?(Prism::SymbolNode) && symbol.unescaped == "nil?"
175
+
176
+ [keep == :falsey ? Narrowing.narrow_non_nil(types.first) : Narrowing.narrow_nil(types.first)]
177
+ end
178
+
179
+ def block_parameter_names(block, arity)
180
+ return nil unless block.is_a?(Prism::BlockNode)
181
+
182
+ parameters = block.parameters
183
+ return nil unless parameters.is_a?(Prism::BlockParametersNode)
184
+
185
+ list = parameters.parameters
186
+ return nil if list.nil? || !simple_parameters?(list) || list.requireds.size != arity
187
+ return nil unless list.requireds.all?(Prism::RequiredParameterNode)
188
+
189
+ list.requireds.map(&:name)
190
+ end
191
+
192
+ def simple_parameters?(list)
193
+ list.optionals.empty? && list.rest.nil? && list.posts.empty? && list.keywords.empty? &&
194
+ list.keyword_rest.nil? && list.block.nil?
195
+ end
196
+
197
+ def block_predicate(block)
198
+ body = block.body
199
+ return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
200
+
201
+ statement = body.body.first
202
+ opaque?(statement) ? nil : statement
203
+ end
204
+
205
+ def opaque?(node)
206
+ return true if OPAQUE_BLOCK_NODES.any? { |klass| node.is_a?(klass) }
207
+
208
+ node.compact_child_nodes.any? { |child| opaque?(child) }
209
+ end
210
+
211
+ def predicate_narrowed(predicate, scope, names, types, keep)
212
+ bound = names.zip(types).reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
213
+ truthy, falsey = Narrowing.predicate_scopes(predicate, bound)
214
+ read = ->(edge) { names.map { |name| edge.local(name) || bound.local(name) } }
215
+ entry_types = read.call(bound)
216
+ truthy_types = read.call(truthy)
217
+ falsey_types = read.call(falsey)
218
+ return nil if truthy_types == entry_types && falsey_types == entry_types
219
+
220
+ keep == :truthy ? truthy_types : falsey_types
221
+ end
222
+ end
223
+ end
224
+ end
@@ -24,7 +24,8 @@ module Rigor
24
24
  # replacement, and the seed it would otherwise keep beside it is gone.
25
25
  REPLACED = Ractor.make_shareable({
26
26
  "Array" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] },
27
- "Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] }
27
+ "Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] },
28
+ "Set" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] }
28
29
  })
29
30
 
30
31
  # Mutators that store values nothing describes BESIDE values they may keep: `fill(x, start)` leaves the slots
@@ -66,6 +67,56 @@ module Rigor
66
67
  Type::Combinator.nominal_of(carrier.class_name, type_args: args)
67
68
  end
68
69
 
70
+ # The element rewriters {.typed} answers for, keyed to the non-bang call whose result is the rewritten
71
+ # receiver: `a.map!(&:to_i)` holds what `a.map(&:to_i)` returns, and `a.flatten!` what `a.flatten` does.
72
+ TYPED_COUNTERPARTS = Ractor.make_shareable({ map!: :map, collect!: :collect, flatten!: :flatten })
73
+
74
+ # The carrier classes {.typed} rewrites, with the rewriters each takes. `Set#flatten!` is absent: it flattens
75
+ # only nested Sets, so `ARGV.map { |x| [x] }.to_set.flatten!` still holds `[String]` Arrays, while `Set#flatten`
76
+ # answers an element its RBS does not narrow.
77
+ TYPED_CARRIERS = Ractor.make_shareable({ "Array" => %i[map! collect! flatten!], "Set" => %i[map! collect!] })
78
+
79
+ # Issue #2427 — `carrier` with its element replaced by `element`, the element of what the non-bang counterpart
80
+ # ({TYPED_COUNTERPARTS}) answers for the same call: `a = ARGV.dup; a.map!(&:to_i)` reads `Array[Integer]` and
81
+ # `a.map!(&:strip)` keeps `Array[String]`. It is the typed form of {.arm}'s {REPLACED} answer, and it replaces
82
+ # the gradual and the precise element alike, since nothing the seed held survives the rewrite.
83
+ #
84
+ # Answers for an `Array` / `Set` nominal and an empty-witness refinement of one (keeping the witness only where
85
+ # {RefinementMutation.preserves_witness?} does: `map!` keeps it, `flatten!` can empty `[[]]`). `nil` for any
86
+ # other carrier or name, so the caller takes the gradual arm instead.
87
+ def typed(carrier, method_name, element)
88
+ return nil unless TYPED_COUNTERPARTS.key?(method_name)
89
+
90
+ case carrier
91
+ when Type::Nominal
92
+ return nil unless carrier.type_args.size == 1 && TYPED_CARRIERS[carrier.class_name]&.include?(method_name)
93
+
94
+ Type::Combinator.nominal_of(carrier.class_name, type_args: [element])
95
+ when Type::Difference
96
+ typed_refinement(carrier, method_name, element)
97
+ end
98
+ end
99
+
100
+ def typed_refinement(difference, method_name, element)
101
+ base = difference.base
102
+ return nil unless difference.removes_empty_witness? && base.is_a?(Type::Nominal)
103
+
104
+ rewritten = typed(base, method_name, element)
105
+ return nil if rewritten.nil?
106
+
107
+ kept = RefinementMutation.preserves_witness?(base.class_name, method_name)
108
+ kept ? Type::Combinator.difference(rewritten, difference.removed) : rewritten
109
+ end
110
+
111
+ # The element of `result`, the non-bang counterpart's answer: an `Array[R]` / `Set[R]` nominal, or an
112
+ # empty-witness refinement of one. `nil` when the answer has no single element to read.
113
+ def counterpart_element(result)
114
+ result = result.base if result.is_a?(Type::Difference) && result.removes_empty_witness?
115
+ return nil unless result.is_a?(Type::Nominal) && %w[Array Set].include?(result.class_name)
116
+
117
+ result.type_args.size == 1 ? result.type_args.first : nil
118
+ end
119
+
69
120
  # {.arm} through a `Union` (member by member) and an empty-witness refinement (its base, keeping the witness
70
121
  # only when the mutator cannot empty the receiver — {RefinementMutation.preserves_witness?}). For a carrier
71
122
  # the ADR-56 slice-C join rebuilt from its seed, which knows the adders' content but not the rewrite.
@@ -114,7 +165,7 @@ module Rigor
114
165
  when Type::Nominal then type.class_name
115
166
  end
116
167
  end
117
- private_class_method :value_carrier_class, :arm_refinement
168
+ private_class_method :value_carrier_class, :arm_refinement, :typed_refinement
118
169
  end
119
170
  end
120
171
  end
@@ -111,7 +111,6 @@ module Rigor
111
111
  node.rigor_each_child { |child| certain(child) }
112
112
  end
113
113
 
114
- # Marks every contribution of the subtree.
115
114
  def possible(node)
116
115
  return unless node.is_a?(Prism::Node)
117
116