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
@@ -20,8 +20,9 @@ module Rigor
20
20
  # The rule is exact rather than heuristic — the receiver must be a `Singleton[C]` whose `C` the project
21
21
  # pre-pass recorded as including `Singleton` — so a class that does not include the mixin is untouched,
22
22
  # and a project that defines its own `Singleton` module is the one case that would misfire. Guarded by
23
- # {project_singleton_shadow?}: a project-declared `Singleton` class or module wins and the tier declines,
24
- # because then the name means something else entirely.
23
+ # {own_singleton_include?}: when the included name resolves to a project-declared class or module (a
24
+ # top-level `Singleton`, or `M::Singleton` inside `module M`), the tier declines, because then the name
25
+ # means something else entirely.
25
26
  module SingletonMixinDispatch
26
27
  MIXIN_NAMES = Ractor.make_shareable(Set["Singleton", "::Singleton"])
27
28
  private_constant :MIXIN_NAMES
@@ -31,30 +32,62 @@ module Rigor
31
32
 
32
33
  module_function
33
34
 
34
- # @return `Nominal[C]` for `C.instance`, or nil to decline.
35
+ # @return the instance type of `C` for `C.instance`, or nil to decline.
35
36
  def try_dispatch(context)
36
- return nil unless context.method_name == SELECTOR
37
- return nil unless context.args.empty?
38
-
39
37
  receiver = context.receiver
40
- return nil unless receiver.is_a?(Type::Singleton)
41
-
42
- scope = context.scope
43
- return nil if scope.nil?
44
- return nil unless includes_singleton_mixin?(scope, receiver.class_name)
45
- return nil if project_singleton_shadow?(scope)
38
+ return nil unless answers?(context.scope, receiver, context.method_name, context.args.empty?)
46
39
 
47
40
  Type::Combinator.nominal_of(receiver.class_name)
48
41
  end
49
42
 
43
+ # Issue #2438 — the one predicate behind {try_dispatch} and `CheckRules#undefined_method_diagnostic`, so the
44
+ # typer and the `call.undefined-method` rule cannot disagree about `C.instance`: a call this tier types is
45
+ # never reported as undefined, whatever a project sidecar of `C` lists.
46
+ def answers?(scope, receiver, method_name, no_arguments)
47
+ return false unless method_name == SELECTOR && no_arguments
48
+ return false unless receiver.is_a?(Type::Singleton)
49
+ return false if scope.nil?
50
+
51
+ includes_singleton_mixin?(scope, receiver.class_name)
52
+ end
53
+
54
+ # True when `class_name`'s instance ancestry carries the stdlib `Singleton` as an `include` written in the
55
+ # class itself or in a project class up its superclass chain (Ruby's `Singleton.included` also installs an
56
+ # `inherited` hook, so `class D < C` answers `instance` with its own instance). Read off
57
+ # `Scope::ResolutionChain` (the `:constants` flavor, so an empty project `module Singleton` is a project
58
+ # entry), where the stdlib module is an external entry. Issue #2438 — what is not the mixin:
59
+ #
60
+ # - `prepend Singleton`, which runs `prepend_features` instead of the `included` hook and so defines no
61
+ # `instance`; a prepended entry sits ahead of its owner on the chain;
62
+ # - an `include Singleton` in a MODULE the class includes: `Singleton.included` raises `TypeError` for a
63
+ # module, so that code never runs;
64
+ # - a top-level project `Singleton`, which both `Singleton` and `::Singleton` name.
65
+ #
66
+ # A NESTED project `N::Singleton` the name resolved to is read as the stdlib mixin: the discovery tables
67
+ # drop the leading `::` of a mixin argument, so `include ::Singleton` and `include Singleton` inside
68
+ # `module N` record the same name, and the stdlib reading is the one that types the rooted spelling and
69
+ # never reports a correct call.
50
70
  def includes_singleton_mixin?(scope, class_name)
51
- scope.includes_of(class_name).any? { |name| MIXIN_NAMES.include?(name.to_s) }
71
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s.delete_prefix("::"), :instance, :constants)
72
+ chain.record(scope)
73
+ classes = chain.level_classes.compact
74
+ chain.entries.each_with_index.any? { |entry, index| singleton_include?(chain, classes, entry, index) }
75
+ end
76
+
77
+ def singleton_include?(chain, classes, entry, index)
78
+ if entry.external?
79
+ !entry.superclass_edge && MIXIN_NAMES.include?(entry.raw.to_s) && classes.include?(entry.owner) &&
80
+ index > chain.index_of(entry.owner)
81
+ else
82
+ entry.name.end_with?("::Singleton") && included_by_level_class?(chain, index)
83
+ end
52
84
  end
53
85
 
54
- # True when the project declares its OWN `Singleton` class / module, in which case `include Singleton`
55
- # names that one and the stdlib rule does not apply.
56
- def project_singleton_shadow?(scope)
57
- MIXIN_NAMES.any? { |name| scope.discovery.discovered_classes.key?(name.to_s.delete_prefix("::")) }
86
+ # The entry sits after its level's class, so it was included into that level rather than prepended.
87
+ def included_by_level_class?(chain, index)
88
+ level = chain.level_starts.rindex { |start| start <= index }
89
+ klass = level && chain.level_classes[level]
90
+ !klass.nil? && index > chain.index_of(klass)
58
91
  end
59
92
  end
60
93
  end
@@ -14,8 +14,8 @@ module Rigor
14
14
  #
15
15
  # A `Struct` instance is mutable (`s.x = v`, `s[:x] = v`, escape), so a member map bound to a
16
16
  # variable can be invalidated by a later write. A member read folds off a **fresh** instance (the
17
- # transient receiver of a `.new(...).x` / `.with(...).x` chain, unmutatable between materialisation
18
- # and the read — slices 1+2), off a **fold-safe stored local** the {Inference::StructFoldSafety} scan
17
+ # transient receiver of a `.new(...).x` chain, unmutatable between materialisation and the read —
18
+ # slices 1+2), off a **fold-safe stored local** the {Inference::StructFoldSafety} scan
19
19
  # proved is never mutated / aliased / escaped (slice 3), or off a **setter-mutated fold-safe local**
20
20
  # whose bindings {#apply_setter_writeback} keeps current (slice 4). Any other stored receiver degrades
21
21
  # to `Dynamic[top]` rather than fold a possibly-stale value.
@@ -255,8 +255,8 @@ module Rigor
255
255
  end
256
256
 
257
257
  # A member read is foldable when the receiver is either FRESH (the transient result of a
258
- # `.new(...)`/`.with(...)` chain, which cannot have been mutated between materialisation and this
259
- # read) or a FOLD-SAFE stored local (ADR-48 slice 3 — `StructFoldSafety` proved the binding is
258
+ # `.new(...)` chain, which cannot have been mutated between materialisation and this read) or a
259
+ # FOLD-SAFE stored local (ADR-48 slice 3 — `StructFoldSafety` proved the binding is
260
260
  # never mutated / aliased / escaped in its scope).
261
261
  def foldable_receiver?(context)
262
262
  fresh_receiver?(context) || fold_safe_local_receiver?(context) ||
@@ -308,7 +308,7 @@ module Rigor
308
308
  end
309
309
 
310
310
  # A fresh receiver is the result of a chained call that MATERIALISED the struct — `Point.new(1, 2).x`,
311
- # `Point[1, 2].x`, `inst.with(x: 9).y`. Not merely a chained call: slices 1-2 read "chained" as
311
+ # `Point[1, 2].x`. Not merely a chained call: slices 1-2 read "chained" as
312
312
  # "cannot have been mutated between materialisation and the read", and a method that hands back its
313
313
  # own receiver falsifies that (issue #595):
314
314
  #
@@ -31,12 +31,10 @@ module Rigor
31
31
  # nodes; the bound exists so a pathological body cannot turn a per-call-site gate into a walk.
32
32
  FACTORY_BODY_SCAN_BUDGET = 200
33
33
 
34
- # Issue #595 / #525 — the ONE materialisation test. Both places that must answer "was this receiver
35
- # expression's struct newly built?" go through it: this module's direct member-read gate above, and
36
- # `ExpressionTyper`'s caller-side `:self`-grant arm. They had forked answers once (the grant arm was
37
- # tightened for #591 while this one still trusted any chained call), so both consumers must share this
38
- # one answer. A `.with` call is never a materialisation: the composed `x.dup_self.with(indent: 9)`
39
- # shape once slipped through as one, and `Struct#with` no longer exists to fold (#1335).
34
+ # Issue #595 / #525 — the ONE materialisation test, shared by {StructFolding#fresh_receiver?} and
35
+ # `ExpressionTyper`'s `:self`-grant arm so the two gates cannot fork. A `.with` call is never a
36
+ # materialisation: the composed `x.dup_self.with(indent: 9)` shape once slipped through as one, and
37
+ # `Struct#with` no longer exists (#1335).
40
38
  #
41
39
  # @param node — the receiver EXPRESSION.
42
40
  # @param scope — the scope the factory and ancestor lookups resolve against.
@@ -55,7 +55,7 @@ module Rigor
55
55
 
56
56
  # A form long enough to suggest generated or accumulated data rather than a literal a reader wrote;
57
57
  # past it the folded `Constant[String]` stops being useful in a diagnostic and only costs memory.
58
- # Mirrors the byte ceiling `ShapeDispatch`'s own `tuple_join` fold applies for the same reason.
58
+ # Counts pairs where `ShapeDispatch`'s `tuple_join` fold caps bytes, for the same reason.
59
59
  FORM_PAIR_LIMIT = 64
60
60
  private_constant :FORM_PAIR_LIMIT
61
61
 
@@ -41,6 +41,7 @@ require_relative "method_dispatcher/universal_object_dispatch"
41
41
  require_relative "method_dispatcher/singleton_mixin_dispatch"
42
42
  require_relative "method_dispatcher/method_folding"
43
43
  require_relative "method_dispatcher/optional_count"
44
+ require_relative "../run_scoped_slots"
44
45
 
45
46
  module Rigor
46
47
  module Inference
@@ -204,8 +205,8 @@ module Rigor
204
205
  # user-authored RBS overrides substrate synthesis) and ABOVE the dependency-source
205
206
  # inference tier so a plugin's declared emit table beats the generic gem-source fallback
206
207
  # for the same class. Slice 6a-TierB (origin_module dispatch) lands precise return types
207
- # for Tier B emissions; Tier C emissions still return `Dynamic[T]` at this tier (slice 6b
208
- # is the Tier C promotion via ADR-13's resolver chain).
208
+ # for Tier B emissions; Tier C emissions return their manifest `return_type` when the environment
209
+ # names that class (slice 6b), and `Dynamic[T]` otherwise.
209
210
  synthetic_result = try_synthetic_method(
210
211
  receiver_type, method_name, arg_types, block_type, environment
211
212
  )
@@ -915,8 +916,7 @@ module Rigor
915
916
  result || EMPTY_CONTRIBUTIONS
916
917
  end
917
918
 
918
- # Runs the precision tiers (constant fold, shape dispatch, file-path fold, block fold) in
919
- # order and returns the first non-nil answer. Each tier owns its own receiver/argument
919
+ # Runs the precision tiers in order and returns the first non-nil answer. Each tier owns its own receiver/argument
920
920
  # shape checks; a tier that does not recognise the receiver returns nil so the next tier
921
921
  # can try. The RBS tier sits below this chain and is invoked by the outer `dispatch`
922
922
  # method.
@@ -925,7 +925,7 @@ module Rigor
925
925
  # (`try_dispatch(CallContext) -> Type?`). Order is significant: ConstantFolding's
926
926
  # exact-value folds win first, the stdlib singleton folders sit in the middle (each
927
927
  # gates on a distinct `Singleton` receiver, so their relative order is immaterial), and
928
- # BlockFolding runs last because its rules only apply to block-taking calls — the cheaper
928
+ # BlockFolding runs near the end because its rules only apply to block-taking calls — the cheaper
929
929
  # arity folds above it filter the common cases first. Adding a precise tier is a one-line
930
930
  # append here rather than another link in a hand-written `||` ladder.
931
931
  PRECISE_TIERS_HEAD = Ractor.make_shareable([
@@ -1031,6 +1031,7 @@ module Rigor
1031
1031
  fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
1032
1032
  return nil if fallback_receiver.nil?
1033
1033
  return nil if declared_by_external_ancestor?(receiver_type, environment, context)
1034
+ return nil if project_singleton_definer?(receiver_type, context)
1034
1035
 
1035
1036
  # Preserve the ORIGINAL receiver type as the `self` substitution so `Kernel#dup: () ->
1036
1037
  # self` and other `self`-returning methods route through Object's RBS while still
@@ -1078,7 +1079,54 @@ module Rigor
1078
1079
  record_dependencies: false).nil?
1079
1080
  end
1080
1081
 
1081
- EXTERNAL_ANCESTOR_MEMO_KEY = :__rigor_user_class_fallback_external_ancestor__
1082
+ # Issue #2284 — a class object whose singleton chain holds a project definer of the name (its own `def
1083
+ # self.name`, one a project superclass wrote, an `extend`ed module's `def name`) answers from that definer,
1084
+ # not from `Module#name` / `Class#…` on the `Singleton[Object]` fallback: Ruby's singleton ancestry puts
1085
+ # every project entry ahead of `Class`. The fallback declines so the typer's singleton-body inference
1086
+ # answers the definer's return, or `Dynamic` where the slot is contested and the def may not be the one
1087
+ # that runs.
1088
+ #
1089
+ # The chain walk files the class edge of every entry it passes, so it runs only for a name some project file
1090
+ # defines at all ({BlockCallTiming.project_defines_anywhere?}, which files the `defines:<name>` edge either way,
1091
+ # #1459). Without it every class-body `private` / `attr_reader` call on a project class would depend on its
1092
+ # whole ancestry. The by-name answer is memoised per discovery index; the edge is filed on every call.
1093
+ def project_singleton_definer?(receiver_type, context)
1094
+ return false unless receiver_type.is_a?(Type::Singleton)
1095
+
1096
+ scope = context.scope
1097
+ return false if scope.nil?
1098
+ return false unless project_defines_name?(scope, context.method_name)
1099
+
1100
+ !scope.singleton_def_through_ancestors(receiver_type.class_name, context.method_name).last.nil?
1101
+ end
1102
+
1103
+ def project_defines_name?(scope, method_name)
1104
+ memo = defines_name_memo(scope)
1105
+ if memo.key?(method_name)
1106
+ Analysis::DependencyRecorder.read_missing(:defines, method_name) if Analysis::DependencyRecorder.active?
1107
+ return memo[method_name]
1108
+ end
1109
+
1110
+ memo[method_name] = BlockCallTiming.project_defines_anywhere?(method_name, scope)
1111
+ end
1112
+
1113
+ DEFINES_NAME_MEMO_KEY = RunScopedSlots.register(:__rigor_user_class_fallback_defines_name__)
1114
+ private_constant :DEFINES_NAME_MEMO_KEY
1115
+
1116
+ # One slot per discovery index and environment, as {#external_ancestor_memo} keeps its own: the answer reads
1117
+ # only the frozen discovery tables and the environment's `pre_eval:` patches.
1118
+ def defines_name_memo(scope)
1119
+ discovery = scope.discovery
1120
+ environment = scope.environment
1121
+ slot = Thread.current[DEFINES_NAME_MEMO_KEY]
1122
+ unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
1123
+ slot = [discovery, environment, {}]
1124
+ Thread.current[DEFINES_NAME_MEMO_KEY] = slot
1125
+ end
1126
+ slot[2]
1127
+ end
1128
+
1129
+ EXTERNAL_ANCESTOR_MEMO_KEY = RunScopedSlots.register(:__rigor_user_class_fallback_external_ancestor__)
1082
1130
  private_constant :EXTERNAL_ANCESTOR_MEMO_KEY
1083
1131
 
1084
1132
  # One slot per discovery index and environment, as `RbsDispatch#ancestor_memo_slot` keeps its own: the answer
@@ -1138,12 +1186,14 @@ module Rigor
1138
1186
  #
1139
1187
  # `RbsDispatch` already distributes over a `Union` (and erases a `Difference` to its base, #533),
1140
1188
  # but it does so INSIDE one tier, so a member that needs a LOWER tier — a user class with no RBS,
1141
- # a discovered `def`, an opt-in gem — makes the whole union decline. That is the corpus shape:
1142
- # rigor-actionpack types `params[:k]` as `ActionController::Parameters | nil`, the `Parameters` arm
1143
- # resolves only through the user-class fallback, and so `params[:k].present?` / `#==` / `#blank?` /
1144
- # `#to_s` read opaque on every Rails app. Measured by toggling this file alone: the named-pair
1145
- # census drains 137 sites on redmine and 56 on mastodon, and `rigor check` reports the same
1146
- # diagnostics on both arms across eight survey targets.
1189
+ # a discovered `def`, an opt-in gem — makes the whole union decline. That was the corpus shape before
1190
+ # #2430: rigor-actionpack typed `params[:k]` as the bare `ActionController::Parameters | nil` union, the
1191
+ # `Parameters` arm resolved only through the user-class fallback, and so `params[:k].present?` / `#==`
1192
+ # / `#blank?` / `#to_s` read opaque on every Rails app. Measured by toggling this file alone: the
1193
+ # named-pair census drained 137 sites on redmine and 56 on mastodon, and `rigor check` reported the
1194
+ # same diagnostics on both arms across eight survey targets. The plugin now answers a `Dynamic`
1195
+ # carrier, which this projection does not reach, so those off-chain calls read `Dynamic[top]` until
1196
+ # the carrier is projected here too (#2448).
1147
1197
  #
1148
1198
  # **Why the bottom and not next to `RbsDispatch`.** Sitting here, the tier can only ever replace a
1149
1199
  # `Dynamic[top]` the typer was about to produce — the same ADR-5 argument {UniversalObjectDispatch}
@@ -1346,8 +1396,8 @@ module Rigor
1346
1396
  # class is representable as the parent's singleton type — its singleton-method surface is
1347
1397
  # the parent's (plus whatever the block defines, which we do not statically track here),
1348
1398
  # so `Singleton[Parent]` lets downstream `klass.some_class_method` resolve. No parent →
1349
- # `singleton(Object)`. Anything else (dynamic parent, more than one positional, …) falls
1350
- # back to `Nominal[Class]` via the surrounding `meta_new` tail.
1399
+ # `singleton(Object)` (or the block's anonymous class, #319 below). Anything else (dynamic parent, more
1400
+ # than one positional, …) falls back to `Nominal[Class]` via the surrounding `meta_new` tail.
1351
1401
  # #319 — a `Class.new do ... end` with no parent is NOT `Object`: the block body is a class body, so the
1352
1402
  # class owns whatever that body defines. Answering `Singleton[Object]` handed the call site `Object`'s
1353
1403
  # zero-arity `new` and `Object`'s method surface, so an `initialize(bucket)` written right there produced
@@ -76,6 +76,17 @@ module Rigor
76
76
  (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
77
77
  end
78
78
 
79
+ # The `def` nodes a receiverless `public`, `private` or `protected` call takes as arguments (`private def x`,
80
+ # #2254). Ruby evaluates each one before the call, so it runs whenever the call's statement does. A named call
81
+ # with a symbol takes no def, and a call with a receiver is not read. A `module_function def` is not read here:
82
+ # its singleton copy is not modelled, so that gap stays as it was.
83
+ def visibility_call_defs(node)
84
+ return [] unless node.is_a?(Prism::CallNode) && node.receiver.nil?
85
+ return [] unless %i[public private protected].include?(node.name)
86
+
87
+ node.arguments&.arguments&.grep(Prism::DefNode) || []
88
+ end
89
+
79
90
  # The mode after `stmt`, a direct statement of a body, for the readers that walk the statements: `mode`
80
91
  # is `true`, `false` or `:uncertain` before it. A {.visibility_reset?} the statement runs ends the mode,
81
92
  # and one it may not run (`:maybe_off`, below {.prescan_deferred}) makes an on mode uncertain. A
@@ -342,7 +353,6 @@ module Rigor
342
353
  true
343
354
  end
344
355
 
345
- # The prescan's call arm.
346
356
  def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
347
357
  if bare?(node)
348
358
  offsets << [node.location.start_offset, :on]
@@ -12,7 +12,7 @@ module Rigor
12
12
  #
13
13
  # `Rigor::Inference::MultiTargetBinder` decomposes a tuple- or array-shaped right-hand side type
14
14
  # against a Prism multi-target tree and produces a `name -> Rigor::Type` binding map. The
15
- # binder is shared between four surfaces:
15
+ # binder is shared between five surfaces:
16
16
  #
17
17
  # 1. `Rigor::Inference::StatementEvaluator#eval_multi_write` for the statement-level `a, b =
18
18
  # rhs` form (`Prism::MultiWriteNode`).
@@ -31,7 +31,12 @@ module Rigor
31
31
  # `nil` when the re-join changed nothing, so a non-adder (`pop`, `sort!`) on an already-widened
32
32
  # carrier does not rebind the binding at all: a rebind drops the per-slot narrowings `with_local`
33
33
  # keys to the old value, and paying that for an identical type is a straight loss.
34
- def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES)
34
+ def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES,
35
+ synthesised: false)
36
+ if synthesised && (synthesised_carrier?(nominal) || partially_gradual_hash?(nominal))
37
+ return widen_synthesised(nominal, method_name, values:, arg_types:)
38
+ end
39
+ return widen_precise_array(nominal, method_name, values:, arg_types:) if precise_array?(nominal)
35
40
  return nil unless regrowable_carrier?(nominal)
36
41
 
37
42
  pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
@@ -115,19 +120,250 @@ module Rigor
115
120
  # This is a superset of "mutation-widened": a DECLARED `Array[untyped]` or
116
121
  # `Array[Integer | untyped]` matches too. Deliberate — the soundness argument above is about
117
122
  # the carrier being open, not about who opened it, and a declared gradual arm is the author
118
- # saying the collection may hold anything. Distinguishing the two needs provenance travelling
119
- # WITH the value (a field on `Type::Nominal`, reopening structural equality, `Marshal`, and the
120
- # cache schema); nothing on this axis needs it yet.
123
+ # saying the collection may hold anything.
124
+ #
125
+ # Issue #2555 L3 — the provenance this boundary turns on travels beside the binding, not with the value: a
126
+ # local whose object the engine built carries the `:synthesised` construction fact ({Scope#construction_fact?}),
127
+ # and {#widen_synthesised} answers for it before this predicate is asked. A field on `Type::Nominal` was the
128
+ # other route; `Combinator.union`'s structural dedupe would keep whichever tagged twin it met first.
121
129
  def regrowable_carrier?(nominal)
122
130
  return false unless %w[Array Hash].include?(nominal.class_name)
123
131
 
124
132
  nominal.type_args.any? { |arg| gradual_parameter?(arg) }
125
133
  end
126
134
 
135
+ # Issue #2555 L3 — a PRECISE `Array` / `Hash` nominal (no parameter carries a `Dynamic` arm), the carrier
136
+ # {#widen_synthesised} re-grows when its binding carries the `:synthesised` fact. A carrier that already carries
137
+ # a gradual arm keeps the ordinary re-join, whose floor it holds already.
138
+ def synthesised_carrier?(nominal)
139
+ %w[Array Hash].include?(nominal.class_name) && !nominal.type_args.empty? &&
140
+ nominal.type_args.none? { |arg| gradual_parameter?(arg) }
141
+ end
142
+
143
+ # Issue #2555 L3 — a `Hash` with one gradual parameter and one precise one: `Hash.new(0)` is
144
+ # `Hash[Dynamic[top], Integer]`. On a synthesised binding the precise side is this engine's own fold of the
145
+ # default, not a declaration, so {#keep_precise_parameters} must not restore it over a store:
146
+ # `h = Hash.new(0); h[:y] = "s"` reads `Hash[Dynamic[top] | Symbol, Integer | String]`, not
147
+ # `Hash[Dynamic[top] | Symbol, Integer]` (which made `h[:y].upcase` an `undefined method` on Integer). The
148
+ # counter idiom keeps its `Integer`: `h[:x] += 1` stores an `Integer`.
149
+ def partially_gradual_hash?(nominal)
150
+ nominal.class_name == "Hash" && nominal.type_args.size == 2 &&
151
+ nominal.type_args.any? { |arg| gradual_parameter?(arg) } &&
152
+ nominal.type_args.any? { |arg| !gradual_parameter?(arg) }
153
+ end
154
+
155
+ # Issue #2555 L3 — the re-join for a precise carrier the engine built itself: a collection literal closed by a
156
+ # value-preserving mutator (`a = [1, 2]; a.sort!` is `Array[1 | 2]`, `g = { a: 1 }; g.delete(:b)` is
157
+ # `Hash[Symbol, 1]`), a constructor fold (`Array.new(n, 0)` is `Array[Integer]`), or a literal built as a
158
+ # nominal outright (`{ ENV["A"].to_s => 1 }`). Its element types are this engine's inference, not a claim a
159
+ # declaration made, so the #561 boundary ({#regrowable_carrier?}, {#precise_admitted}) does not apply:
160
+ #
161
+ # - an added member joins as itself (value pin and literal shape dropped, {MutationWidening.value_pin_widened}),
162
+ # with no seed-admissibility gate and no `Dynamic[top]` floor — `Array.new(n, 0) << "s"` reads
163
+ # `Array[Integer | String]`;
164
+ # - a rewriter takes {RewriteMutation.arm}'s gradual position, as a gradual carrier does —
165
+ # `g.transform_values!(&:to_s)` reads `Hash[Symbol, Dynamic[top]]`;
166
+ # - an adder whose arguments yield no evidence is one unknown store and joins `Dynamic[top]`; one called with no
167
+ # argument types at all (the block-capture seam, whose content join adds the stores afterwards) changes
168
+ # nothing.
169
+ #
170
+ # The floor the ordinary re-join adds exists because a carrier it closed could not re-grow; this one stays
171
+ # re-growable while the binding keeps the fact, so every later store made THROUGH THE BINDING is seen. A store
172
+ # through another holder is not, which is why a local that may escape anywhere in its body never carries the
173
+ # fact ({ConstructionEscape}). Nor is a remover taking a store out again, which is why a remover on a carrier a
174
+ # store has grown restores the floor ({MutationWidening.floor_grown_carrier}).
175
+ def widen_synthesised(nominal, method_name, values:, arg_types:)
176
+ pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
177
+ rejoined =
178
+ case nominal.class_name
179
+ when "Array"
180
+ return nil unless MutationWidening::ARRAY_MUTATORS.include?(method_name)
181
+
182
+ synthesised_array_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
183
+ when "Hash"
184
+ return nil unless MutationWidening::HASH_MUTATORS.include?(method_name)
185
+
186
+ synthesised_hash_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
187
+ end
188
+ rejoined == nominal ? nil : rejoined
189
+ end
190
+
191
+ # Issue #2555 — the removers and filters of an Array, a Hash or a Set: each can take a stored member out again,
192
+ # which nothing narrows the carrier back from.
193
+ SYNTHESISED_REMOVERS = %i[
194
+ pop shift slice! delete delete? delete_at delete_if reject! select! filter! keep_if compact! clear subtract
195
+ ].to_set.freeze
196
+
197
+ # The adders whose store marks a synthesised binding `:grown`.
198
+ def content_adder?(method_name)
199
+ ContentJoin::CONTENT_ADDERS.include?(method_name) || SetMutation::CONTENT_ADDERS.include?(method_name)
200
+ end
201
+
202
+ # Whether a store turned `current` into `widened` by changing an Array / Hash / Set parameter that carried no
203
+ # `Dynamic` arm before it: a member joined where no floor covered it, which a later remover could leave stale.
204
+ # A store the parameter already held, or one that joined beside an earlier floor (`Hash.new(0)`'s key side),
205
+ # grows nothing.
206
+ def grew_unfloored?(current, widened)
207
+ return false if widened.nil?
208
+
209
+ before = ContentJoin.union_members(current)
210
+ ContentJoin.union_members(widened).any? do |member|
211
+ next false unless member.is_a?(Type::Nominal) && %w[Array Hash Set].include?(member.class_name)
212
+
213
+ prior = before.find { |b| same_carrier?(b, member) }
214
+ member.type_args.each_with_index.any? do |arg, i|
215
+ prior.nil? || (!gradual_parameter?(prior.type_args[i]) && prior.type_args[i] != arg)
216
+ end
217
+ end
218
+ end
219
+
220
+ def same_carrier?(type, nominal)
221
+ type.is_a?(Type::Nominal) && type.class_name == nominal.class_name &&
222
+ type.type_args.size == nominal.type_args.size
223
+ end
224
+
225
+ # Whether `method_name` on the synthesised local `name` takes the floor: a remover on a carrier a store grew.
226
+ def floors_grown_carrier?(scope, name, method_name)
227
+ SYNTHESISED_REMOVERS.include?(method_name) && scope.construction_fact?(name, :grown)
228
+ end
229
+
230
+ # `type` with every Array / Hash / Set parameter joined with `Dynamic[top]`, member-wise through a union: the
231
+ # reading the carrier would have had without the fact. With `drop_nil` (a remover that can take a `nil` out,
232
+ # {MutationWidening.removes_nil?}) the parameter's `nil` member goes too: on the path without the fact a stored
233
+ # `nil` the seed did not accept joined as the floor alone ({#precise_admitted}), and a `nil` kept beside the floor
234
+ # would still draw `call.possible-nil-receiver` after `parts << nil; parts.compact!`. Any other remover keeps the
235
+ # `nil`, which it may leave behind (`s << nil; s.delete(1)`).
236
+ def floor_parameters(type, drop_nil: false)
237
+ case type
238
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| floor_parameters(member, drop_nil:) })
239
+ when Type::Nominal
240
+ return type unless %w[Array Hash Set].include?(type.class_name) && !type.type_args.empty?
241
+
242
+ Type::Combinator.nominal_of(type.class_name,
243
+ type_args: type.type_args.map { |arg| floored_parameter(arg, drop_nil) })
244
+ else type
245
+ end
246
+ end
247
+
248
+ def hash_carrier?(type)
249
+ ContentJoin.union_members(type).any? { |member| member.is_a?(Type::Nominal) && member.class_name == "Hash" }
250
+ end
251
+
252
+ def floored_parameter(arg, drop_nil)
253
+ kept = ContentJoin.union_members(arg)
254
+ kept = kept.reject { |member| member.is_a?(Type::Constant) && member.value.nil? } if drop_nil
255
+ Type::Combinator.union(*kept, Type::Combinator.untyped)
256
+ end
257
+
258
+ def synthesised_array_join(carrier, method_name, arg_types)
259
+ return carrier unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
260
+
261
+ added = MutationWidening.value_pin_widened(ContentJoin.array_added_elements(method_name, arg_types))
262
+ ContentJoin.join_array_content(carrier, added.empty? ? [Type::Combinator.untyped] : added)
263
+ end
264
+
265
+ def synthesised_hash_join(carrier, method_name, arg_types)
266
+ return carrier unless ContentJoin::HASH_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
267
+
268
+ # A `nil` key marks a splat, an unknown key ({MutationWidening.join_added_pairs}).
269
+ key, value = MutationWidening.value_pin_widened([arg_types.first || Type::Combinator.untyped, arg_types.last])
270
+ value ||= Type::Combinator.untyped
271
+ ContentJoin.join_hash_content(carrier, [[key, value]])
272
+ end
273
+
127
274
  def gradual_parameter?(type)
128
275
  ContentJoin.union_members(type).any?(Type::Dynamic)
129
276
  end
130
277
 
278
+ # A typed `Array[E]` whose `E` has no `Dynamic` arm — the carrier {#regrowable_carrier?} refuses.
279
+ def precise_array?(nominal)
280
+ nominal.class_name == "Array" && nominal.type_args.size == 1 && !gradual_parameter?(nominal.type_args.first)
281
+ end
282
+
283
+ # Issue #2361 — a PRECISE `Array[E]` re-grows under a store, on the literal arms' terms: `a = ARGV.dup;
284
+ # a.clear; a << 1; a.each { |v| v.fdiv(2) }` read `Array[String]` and reported `undefined method 'fdiv' for
285
+ # String` on code that prints `[0.5]`.
286
+ #
287
+ # Only a content adder ({ContentJoin::ARRAY_CONTENT_ADDERS}) re-grows it, and the join keeps the seed: `clear`
288
+ # empties the array but leaves `E` standing, a superset of what it holds. A remover leaves the carrier as it
289
+ # found it. An element rewriter (`map!`, `collect!`, `flatten!`) replaces `E` (issue #2427): with the element
290
+ # the non-bang counterpart answers when the call site could type it ({RewriteMutation.typed}, answered before
291
+ # this method), and otherwise with {RewriteMutation.arm}'s gradual element — `a.map!.with_index { … }` holds
292
+ # whatever the block returns, and keeping the stale `E` reported a missing method on correct code.
293
+ #
294
+ # Each added member the element accepts joins as itself; any other takes the `Dynamic[top]` floor
295
+ # ({#precise_admitted}), and the floor joins every re-grow as it does on the literal arms. `a << 1` therefore
296
+ # reads `Array[Dynamic[top] | String]` and `s = ints.map { "x" }; s << "y"` reads `Array["x" | Dynamic[top]]`.
297
+ # Nothing narrows a carrier back when a remover or rewriter takes the store out again (`a << 1; a.pop`,
298
+ # `a << nil; a.compact!`, `a.map!(&:to_s)`), so a joined `Integer` or `nil` made `t.take(a)` against a declared
299
+ # `Array[String]` report `call.argument-type-mismatch`, and a parameter returned as its declared
300
+ # `-> Array[String]` report `def.return-type-mismatch`, on correct Ruby. A union carrying the floor neither folds
301
+ # nor dispatches loudly, so the read stays silent. Once floored, the carrier is gradual and later stores re-join
302
+ # through {#widen_nominal}'s ordinary arm.
303
+ #
304
+ # A store the element already accepts changes nothing ({#already_held?}). An adder with no argument types at
305
+ # all is left alone, as {MutationWidening.join_added_elements} leaves it: the block-capture seams pass none,
306
+ # and their own content join adds the stores.
307
+ def widen_precise_array(nominal, method_name, values:, arg_types:)
308
+ return RewriteMutation.arm(nominal, method_name) if RewriteMutation::TYPED_COUNTERPARTS.key?(method_name)
309
+ return nil unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
310
+
311
+ stored = ContentJoin.array_added_elements(method_name, arg_types)
312
+ return nil if already_held?(nominal.type_args.first, stored)
313
+
314
+ added = MutationWidening.value_pin_widened(stored)
315
+ admitted = precise_admitted(nominal.type_args.first, added)
316
+ pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
317
+ rejoined = ContentJoin.join_array_content(pre_state, MutationWidening.gradual_floor(admitted))
318
+ rejoined == nominal ? nil : rejoined
319
+ end
320
+
321
+ # The seed-admissibility gate for a precise element: an added member joins as itself only when `element`
322
+ # already accepts it, and takes the `Dynamic[top]` floor otherwise. A precise element therefore never gains a
323
+ # member it rejects, which is what a declared boundary checks: nothing narrows the carrier back when a remover
324
+ # or rewriter takes the store out again, and a joined foreign arm stays rejected beside the floor — `Integer`
325
+ # under a declared `Array[String]`, `String` under `-> Array["x"]`, `Array[Integer]` under
326
+ # `Array[Array[String]]` each reported on correct Ruby. A class set ({ContentJoin.admissible_evidence}) is too
327
+ # coarse for that: it admits `Array[Integer]` beside `[String]` and `"y"` beside `"x"`.
328
+ def precise_admitted(element, added)
329
+ added.flat_map do |type|
330
+ ContentJoin.union_members(type).map do |member|
331
+ Acceptance.accepts(element, member).yes? ? member : Type::Combinator.untyped
332
+ end
333
+ end
334
+ end
335
+
336
+ # True when the precise element `element` already covers every type `stored` puts in, so the store changes
337
+ # nothing: `s = ints.map { "x" }; s << "x"` keeps `Array["x"]` and its `s.last == "x"` fold, which is correct,
338
+ # and a `[Integer, Integer]` pushed onto an `Array[[0 | Integer, Integer]]` keeps the Tuple element. Joining
339
+ # the value-pin-widened, shape-erased store instead would only lose precision.
340
+ #
341
+ # A stored type carrying `Dynamic` anywhere is never covered, though `Acceptance` passes it gradually: keeping
342
+ # the element would claim the unknown part is what the element says (`retry_binding_accepted?`'s rule).
343
+ def already_held?(element, stored)
344
+ return false if stored.empty?
345
+
346
+ held = ContentJoin.union_members(element)
347
+ stored.all? do |type|
348
+ next false if type.nil? || carries_dynamic?(type)
349
+
350
+ ContentJoin.union_members(type).all? { |member| held.include?(member) } ||
351
+ Acceptance.accepts(element, type).yes?
352
+ end
353
+ end
354
+
355
+ def carries_dynamic?(type)
356
+ case type
357
+ when Type::Dynamic then true
358
+ when Type::Union, Type::Intersection then type.members.any? { |member| carries_dynamic?(member) }
359
+ when Type::Nominal then type.type_args.any? { |arg| carries_dynamic?(arg) }
360
+ when Type::Tuple then type.elements.any? { |element| carries_dynamic?(element) }
361
+ when Type::HashShape then type.pairs.each_value.any? { |value| carries_dynamic?(value) }
362
+ when Type::Difference, Type::Refined then carries_dynamic?(type.base)
363
+ else false
364
+ end
365
+ end
366
+
131
367
  # The value-rewriting counterpart of {MutationWidening.widen_tuple}'s `values: :widen`. A mutator that lands a
132
368
  # new value in an EXISTING slot falsifies that slot's pinning on a re-opened carrier exactly as
133
369
  # it does on a literal one, so `d = [:multi]; d << x; d[0] = "s"` must not keep reading `:multi`