rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../typing_state"
6
+ require_relative "../source/node_children"
7
+ require_relative "closure_escape_analyzer"
8
+ require_relative "construction_escape"
9
+ require_relative "../memo_audit"
10
+
11
+ module Rigor
12
+ module Inference
13
+ # Issue #2608, review rounds 1 and 2 of #2621 — which locals of a body another holder may write, so a remover on
14
+ # them is not narrowed ({RemoverNarrowing}): a narrowing is a fact about the object, and a write through any other
15
+ # holder after the call would outlive it. One flow-insensitive scan per frame body; a local is a holder's when:
16
+ #
17
+ # - a closure that may run later references it: a lambda, or a block passed to a call that is not a catalogued
18
+ # iterator or `tap` / `then` / `yield_self` (`proc { l << 1 }`), created before or after the remover;
19
+ # - it is read anywhere inside the value of any write (local, instance, class, global or constant variable, an
20
+ # index or attribute write, an operator or `||=` / `&&=` write, a multiple assignment) or inside the subject of a
21
+ # `case`/`in`, `=>` or `in` pattern: `al = c ? l : []`, `al = l.push(1)`, `@k = l`, `case l; in al`. The local
22
+ # such a write or pattern binds is a holder too (both names of a copy), unless the write's value is a fresh
23
+ # object: an Array or Hash literal, or `new` / `[]` on a bare or top-level `Array`, `Hash` or `Set`
24
+ # (`a = Array.new(n, "a")`);
25
+ # - a method's parameter, when its body has a zero-argument `super`, which passes every parameter on;
26
+ # - it is read inside an Array element, a Hash key or value, a splat, or an argument of any call, `yield` or
27
+ # `super` (positional, keyword, splat or block-pass), unless the read is the receiver of a call that answers
28
+ # a value other than the receiver and keeps no reference to it ({PURE_READERS}: #2555's per-class
29
+ # `ConstructionEscape::READERS`, less the conversions that return self on another class): `puts(l.size)`,
30
+ # `dump_type(l.dup)` and `dump_type(l.first)` hold nothing, `Keep.new(l)` and `foo(l.push(1))` do;
31
+ # - it receives a reflective call (`l.send(...)`, `l.instance_eval { }`, `l.tap { }`);
32
+ # - and every local, when the body calls `binding`, `eval`, `local_variable_get` or `local_variable_set` on any
33
+ # receiver (`-> {}.binding`), or a receiverless `instance_variable_set`.
34
+ #
35
+ # A receiver position, a `return` and a body's last expression hand nothing on.
36
+ module RemoverHolderScan
37
+ REFLECTIVE_CALLS = %i[
38
+ send public_send __send__ instance_eval instance_exec tap then yield_self itself method
39
+ define_singleton_method
40
+ ].to_set.freeze
41
+ # A conversion one class lists as a reader returns the receiver itself on another (`Array#to_a`, `Hash#to_h`,
42
+ # `Set#to_set`), so the union of the per-class lists leaves them out.
43
+ SELF_CONVERSIONS = %i[to_a to_h to_set].freeze
44
+ PURE_READERS = (ConstructionEscape::READERS.values.reduce(:|) - SELF_CONVERSIONS).freeze
45
+ IMMEDIATE_BLOCK_CALLS = %i[tap then yield_self].to_set.freeze
46
+ FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
47
+ # Frame calls that reach the locals whatever their receiver (`pr.binding`, `Kernel.eval`).
48
+ ANY_RECEIVER_FRAME_CALLS = %i[binding eval local_variable_get local_variable_set].to_set.freeze
49
+ NON_WRITER_OPERATORS = %i[== != <= >= ===].to_set.freeze
50
+ CORE_COLLECTIONS = %i[Array Hash Set].to_set.freeze
51
+ LOCAL_REFERENCES = [
52
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
53
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
54
+ ].freeze
55
+ LOCAL_WRITES = [
56
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
57
+ Prism::LocalVariableAndWriteNode
58
+ ].freeze
59
+ ALL = :all
60
+ NO_ARGUMENTS = [].freeze
61
+ # ADR-129 WD1 — a run-scoped Memo, emptied when a run ends so no verdict outlives the body it read: keyed by
62
+ # body node, and the unsafe names are a pure function of the body and its frame's parameters.
63
+ UNSAFE_CACHE = TypingState.memo(
64
+ :remover_narrowing_unsafe,
65
+ run_scoped: true, audit: "remover narrowing unsafe",
66
+ reason: "keyed by the body node; the unsafe local names are a pure function of the body"
67
+ )
68
+ private_constant :REFLECTIVE_CALLS, :SELF_CONVERSIONS, :PURE_READERS, :IMMEDIATE_BLOCK_CALLS, :FRAME_CALLS,
69
+ :ANY_RECEIVER_FRAME_CALLS, :NON_WRITER_OPERATORS, :CORE_COLLECTIONS,
70
+ :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :NO_ARGUMENTS, :UNSAFE_CACHE
71
+
72
+ module_function
73
+
74
+ # Whether the local `name` may be written through another holder somewhere in the body `scope` runs. A scope
75
+ # with no known body answers true.
76
+ def unsafe_receiver?(scope, name)
77
+ frame = scope.match_frame
78
+ body = frame&.body
79
+ return true if body.nil?
80
+
81
+ names = memoised_unsafe_names(body, frame.parameters)
82
+ names.equal?(ALL) || names.include?(name.to_sym)
83
+ end
84
+
85
+ # {.unsafe_names} of `body`, memoised for the run.
86
+ def memoised_unsafe_names(body, parameters)
87
+ return unsafe_names(body, parameters) if MemoAudit.enabled? && MemoAudit.suspended?
88
+
89
+ table = TypingState.current.weak_key_map(UNSAFE_CACHE)
90
+ stored = table[body]
91
+ if stored.nil?
92
+ return table[body] = MemoAudit.key_audit("remover narrowing unsafe") { unsafe_names(body, parameters) }
93
+ end
94
+
95
+ MemoAudit.check("remover narrowing unsafe", stored, body) { unsafe_names(body, parameters) }
96
+ end
97
+
98
+ # `names` collects the locals; a collector that answers `site=` ({AliasMutationFloor}) is told, before each name
99
+ # it is given, the node that hands the local on, or nil when no single node does (a capture by a closure that may
100
+ # run later, a zero-argument `super`), reads value positions itself when it answers `value_held` ({.precise?}),
101
+ # and is told when the walk enters (`enter(node, parent)`) and leaves
102
+ # (`leave(node)`) each node. One that answers `keeps_arguments?` says which calls' plain arguments it counts as
103
+ # held.
104
+ def unsafe_names(body, parameters = nil, names = Set.new)
105
+ sited = names.respond_to?(:site=)
106
+ catch(ALL) do
107
+ collect_unsafe(body, false, names, sited:)
108
+ names.site = nil if sited
109
+ parameter_names(parameters, names) if forwards_parameters?(body)
110
+ return names.freeze
111
+ end
112
+ ALL
113
+ end
114
+
115
+ def collect_unsafe(node, in_closure, names, sited: false, parent: nil)
116
+ in_closure ||= node.is_a?(Prism::LambdaNode)
117
+ if sited
118
+ names.enter(node, parent)
119
+ names.site = in_closure ? nil : node
120
+ end
121
+ names << node.name if in_closure && LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
122
+ record_node(node, names)
123
+
124
+ node.rigor_each_child do |child|
125
+ child_in_closure = in_closure || (child.is_a?(Prism::BlockNode) && deferred_block?(node))
126
+ collect_unsafe(child, child_in_closure, names, sited:, parent: node)
127
+ end
128
+ names.leave(node) if sited
129
+ end
130
+
131
+ def record_node(node, names) # rubocop:disable Metrics/CyclomaticComplexity
132
+ case node
133
+ when Prism::CallNode then record_call(node, names)
134
+ when Prism::YieldNode, Prism::SuperNode then node.arguments&.arguments&.each { |a| record_held(a, names) }
135
+ when Prism::ArrayNode then node.elements.each { |element| record_held(element, names) }
136
+ when Prism::AssocNode
137
+ record_held(node.key, names)
138
+ record_held(node.value, names)
139
+ when Prism::SplatNode, Prism::BlockArgumentNode then record_held(node.expression, names)
140
+ when Prism::CaseMatchNode then record_pattern(node.predicate, node.conditions, names)
141
+ when Prism::MatchRequiredNode, Prism::MatchPredicateNode then record_pattern(node.value, [node.pattern], names)
142
+ when Prism::MultiWriteNode then record_multi_write(node, names)
143
+ else record_write(node, names)
144
+ end
145
+ end
146
+
147
+ def record_call(node, names)
148
+ throw ALL if frame_call?(node)
149
+
150
+ receiver = unparenthesised(node.receiver)
151
+ names << receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && REFLECTIVE_CALLS.include?(node.name)
152
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
153
+ if writer_call?(node) && !arguments.empty?
154
+ record_writer_call(arguments, names)
155
+ elsif keeps_arguments?(node, names)
156
+ arguments.each { |argument| record_held(argument, names) }
157
+ end
158
+ end
159
+
160
+ def record_writer_call(arguments, names)
161
+ *keys, value = arguments
162
+ keys.each { |key| record_held(key, names) }
163
+ precise?(names) ? names.value_held(value) : every_read(value, names)
164
+ end
165
+
166
+ def keeps_arguments?(node, names) = !names.respond_to?(:keeps_arguments?) || names.keeps_arguments?(node)
167
+
168
+ # A collector that reads a value position itself ({AliasMutationFloor}): `value_held(node)` for a value handed
169
+ # to another holder (an element, an argument, an index or attribute write's value), `value_written(node)` for a
170
+ # write node, and `value_bound(value, targets)` for a multiple assignment or a pattern. The default collector
171
+ # reads every local inside the value instead.
172
+ def precise?(names) = names.respond_to?(:value_held)
173
+
174
+ # A zero-argument `super` passes every parameter of the method on, implicitly. (A `...` forward passes only the
175
+ # anonymous parameters, which name no local.)
176
+ def forwards_parameters?(node)
177
+ return true if node.is_a?(Prism::ForwardingSuperNode)
178
+
179
+ node.rigor_each_child { |child| return true if forwards_parameters?(child) }
180
+ false
181
+ end
182
+
183
+ def parameter_names(node, names)
184
+ return if node.nil?
185
+
186
+ names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
187
+ node.rigor_each_child { |child| parameter_names(child, names) }
188
+ end
189
+
190
+ def frame_call?(node)
191
+ ANY_RECEIVER_FRAME_CALLS.include?(node.name) || (node.receiver.nil? && FRAME_CALLS.include?(node.name))
192
+ end
193
+
194
+ def unparenthesised(node)
195
+ node = node.body.body.first while node.is_a?(Prism::ParenthesesNode) && node.body&.body&.size == 1
196
+ node
197
+ end
198
+
199
+ # An index or attribute write (`h[k] = v`, `o.x = v`, `o&.x = v`): its value is a write's value.
200
+ def writer_call?(node)
201
+ name = node.name
202
+ name.end_with?("=") && !NON_WRITER_OPERATORS.include?(name)
203
+ end
204
+
205
+ # A write's value hands on every local read anywhere inside it, and the local it binds holds what it read.
206
+ def record_write(node, names)
207
+ return unless node.class.name.end_with?("WriteNode") && node.respond_to?(:value)
208
+ return names.value_written(node) if precise?(names)
209
+ return unless every_read(node.value, names)
210
+
211
+ names << node.name if LOCAL_WRITES.any? { |k| node.is_a?(k) } && !fresh_value?(node.value)
212
+ end
213
+
214
+ # `Array`, `Hash` or `Set`, bare or as a top-level path (`::Set`): a project class may define its own `self.[]` or
215
+ # `self.new` that returns its argument.
216
+ def core_collection_constant?(node)
217
+ top_level = node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)
218
+ top_level && CORE_COLLECTIONS.include?(node.name)
219
+ end
220
+
221
+ # A value that is a new object whatever it reads, so the local it binds aliases nothing.
222
+ def fresh_value?(value)
223
+ case value
224
+ when Prism::ArrayNode, Prism::HashNode then true
225
+ when Prism::CallNode then %i[new []].include?(value.name) && core_collection_constant?(value.receiver)
226
+ else false
227
+ end
228
+ end
229
+
230
+ def record_multi_write(node, names)
231
+ return names.value_bound(node.value, [*node.lefts, node.rest, *node.rights]) if precise?(names)
232
+ return unless every_read(node.value, names)
233
+
234
+ local_targets([*node.lefts, node.rest, *node.rights], names)
235
+ end
236
+
237
+ def record_pattern(subject, patterns, names)
238
+ return names.value_bound(subject, patterns) if precise?(names)
239
+ return unless every_read(subject, names)
240
+
241
+ local_targets(patterns, names)
242
+ end
243
+
244
+ # Adds every local `node` reads, at any depth; answers whether it read any.
245
+ def every_read(node, names)
246
+ return false if node.nil?
247
+
248
+ found = node.is_a?(Prism::LocalVariableReadNode)
249
+ names << node.name if found
250
+ node.rigor_each_child { |child| found = true if every_read(child, names) }
251
+ found
252
+ end
253
+
254
+ def local_targets(nodes, names)
255
+ nodes.compact.each do |node|
256
+ names << node.name if node.is_a?(Prism::LocalVariableTargetNode)
257
+ local_targets(node.compact_child_nodes, names)
258
+ end
259
+ end
260
+
261
+ # A held position: every local read inside it but the receiver of a {PURE_READERS} call.
262
+ def record_held(node, names)
263
+ return if node.nil?
264
+ return names.value_held(node) if precise?(names)
265
+ return if pure_read?(node)
266
+
267
+ names << node.name if node.is_a?(Prism::LocalVariableReadNode)
268
+ node.rigor_each_child { |child| record_held(child, names) }
269
+ end
270
+
271
+ # A {PURE_READERS} call on a local hands the local on to nothing. Its own arguments are held positions of the
272
+ # call itself, which the body walk records when it reaches that call.
273
+ def pure_read?(node)
274
+ node.is_a?(Prism::CallNode) && PURE_READERS.include?(node.name) &&
275
+ node.receiver.is_a?(Prism::LocalVariableReadNode)
276
+ end
277
+
278
+ # A block that may run after its call returns: any but a catalogued iterator's, or `tap` / `then` /
279
+ # `yield_self`'s.
280
+ def deferred_block?(call_node)
281
+ return true unless call_node.is_a?(Prism::CallNode)
282
+
283
+ !(ClosureEscapeAnalyzer.iterator_name?(call_node.name) || IMMEDIATE_BLOCK_CALLS.include?(call_node.name))
284
+ end
285
+ end
286
+ end
287
+ 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
 
@@ -6,8 +6,9 @@ module Rigor
6
6
  module Inference
7
7
  module ScopeIndexer
8
8
  # The `class_cvars` discovery table (slice 7 phase 6) as a {DeclarationWalk} collector: every
9
- # `@@x = …` a `def` body writes, typed under the census scope and unioned per class. Ruby cvars are
10
- # shared by the instance and singleton facets, so every `def` counts, `def self.x` included.
9
+ # `@@x = …` a `def` body writes, typed under the census scope and unioned per class, joined by what a
10
+ # `class` / `module` body writes to the same cvar. Ruby cvars are shared by the instance and singleton
11
+ # facets, so every `def` counts, `def self.x` included.
11
12
  class ClassCvarsCollector
12
13
  include DeclarationWalk::Collector
13
14
 
@@ -15,6 +16,14 @@ module Rigor
15
16
  @table = {}
16
17
  end
17
18
 
19
+ # Issue #3204 — a `class` / `module` body's own writes, outside its `def`s, under the body's context;
20
+ # each only joins an entry a `def` body's write creates. An unnameable cref (a header below
21
+ # `class << expr`) leaves the prefix empty, and the gather skips it.
22
+ def on_declaration(node, _context, body)
23
+ ScopeIndexer.collect_body_cvar_writes(node.body, body.prefix, body.scope, @table)
24
+ DeclarationWalk::DESCEND
25
+ end
26
+
18
27
  # `@@x` in a `def` resolves through the LEXICAL cref: `Module.nesting` is the same inside a meta-new or
19
28
  # eval block, so the context's `prefix` keys the write and its rebound `self` never does —
20
29
  # `K = Class.new { def m = @@x }` inside `class C` writes `C::@@x`. The walk goes no further in: the
@@ -25,10 +34,8 @@ module Rigor
25
34
  DeclarationWalk::DECLINE
26
35
  end
27
36
 
28
- # The table as the discovery index holds it: each class's entry frozen, then the whole.
29
- def table
30
- @table.transform_values(&:freeze).freeze
31
- end
37
+ # The table as the discovery index holds it ({ScopeIndexer#finished_cvar_census}).
38
+ def table = ScopeIndexer.finished_cvar_census(@table)
32
39
  end
33
40
  end
34
41
  end