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
@@ -9,6 +9,8 @@ require_relative "../type"
9
9
  require_relative "guard_rebinding"
10
10
  require_relative "project_method_ownership"
11
11
  require_relative "refined_dispatch/own_method"
12
+ require_relative "../typing_state"
13
+ require_relative "../memo_audit"
12
14
 
13
15
  module Rigor
14
16
  module Inference
@@ -85,7 +87,9 @@ module Rigor
85
87
  :to_r, :to_c, :to_proc, :to_path, :to_sym, :to_io, :coerce, :exception, :divmod, :each, :respond_to?,
86
88
  :respond_to_missing?, :method_missing, :initialize_copy, :marshal_dump, :pretty_print
87
89
  ].freeze
88
- private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES
90
+ # Issue #2203 — the classes and module every project class's instances reach past its own chain.
91
+ IMPLICIT_ROOTS = %w[Object Kernel BasicObject].freeze
92
+ private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES, :IMPLICIT_ROOTS
89
93
 
90
94
  # Raised inside a proof to abandon it.
91
95
  class Unprovable < StandardError
@@ -113,6 +117,50 @@ module Rigor
113
117
  nil
114
118
  end
115
119
 
120
+ # ADR-129 WD1 — a run-scoped Memo: the `[deferred_ranges, counts]` pair is one table and the counts computed
121
+ # from it alone, so the counts are a function of the table identity the pair is keyed on.
122
+ DEFINITION_COUNTS = TypingState.memo(
123
+ :self_call_ivar_writes_definition_counts,
124
+ run_scoped: true, audit: "self-call ivar definition counts",
125
+ reason: "the instance-def counts of one deferred-ranges table; keyed on its identity"
126
+ )
127
+ private_constant :DEFINITION_COUNTS
128
+
129
+ # `{[owner, name] => the number of instance `def` rows of name the project records for owner}`, over every
130
+ # file of `deferred_ranges` (a `discovered_deferred_ranges` table). Built once per table and kept in ONE
131
+ # thread slot keyed by the table's identity (#2300): the table is never written in place, a file whose rows
132
+ # equal the seed's keeps the seed's table, and a scan of every file's rows per query was the cost once the
133
+ # table held the whole project. The slot holds one table and its counts, the next table replaces them, and the
134
+ # run that filled it releases it when it ends (run-scoped, #2399).
135
+ def definition_counts(deferred_ranges)
136
+ return count_definitions(deferred_ranges) if MemoAudit.enabled? && MemoAudit.suspended?
137
+
138
+ state = TypingState.current
139
+ slot = state[DEFINITION_COUNTS]
140
+ if slot && slot[0].equal?(deferred_ranges)
141
+ return MemoAudit.check("self-call ivar definition counts", slot[1], deferred_ranges.size) do
142
+ count_definitions(deferred_ranges)
143
+ end
144
+ end
145
+
146
+ counts = MemoAudit.key_audit("self-call ivar definition counts") { count_definitions(deferred_ranges) }
147
+ state[DEFINITION_COUNTS] = [deferred_ranges, counts]
148
+ counts
149
+ end
150
+
151
+ def count_definitions(deferred_ranges)
152
+ counts = {}
153
+ deferred_ranges.each_value do |rows|
154
+ rows.each do |row|
155
+ next unless row[3] == :instance
156
+
157
+ key = [row[4], row[2]]
158
+ counts[key] = counts.fetch(key, 0) + 1
159
+ end
160
+ end
161
+ counts
162
+ end
163
+
116
164
  # One proof's state: the instance variables read code writes, and the `def` bodies already read.
117
165
  class Proof
118
166
  attr_reader :written
@@ -128,7 +176,7 @@ module Rigor
128
176
  # Reads the method an implicit-self or `self.` call to `call_node`'s name runs.
129
177
  def self_call(call_node)
130
178
  name = call_node.name
131
- unprovable! if GuardRebinding.code_running_name?(name) || refined?(name)
179
+ unprovable! if GuardRebinding.code_running_name?(name) || refined?(name, on_self: true)
132
180
 
133
181
  if ProjectMethodOwnership.defines?(@class_name, name, :instance, @scope)
134
182
  read_def(proven_def(name))
@@ -185,19 +233,66 @@ module Rigor
185
233
 
186
234
  private
187
235
 
188
- # Whether a `refine` block anywhere in the project defines `name`, on any class: a `using` in effect where the
189
- # read code sits may route the call to it (`refine Kernel do def warn …`), whatever the receiver.
236
+ # Whether a `refine` block in the project defines `name` where the call may reach it: a `using` in effect
237
+ # where the read code sits may route the call to it (`refine Kernel do def warn …`).
190
238
  #
191
239
  # Issue #2163 — the answer reads every file's refinements whole, and a refined class off the chain (`Kernel`,
192
240
  # `IO`, `String`) is no `mark:` the chain read files, so it records `refinement:<name>` and `refinement:*`
193
241
  # whichever way it answers, as `InEffectRefinements` consumers do: a file that starts or stops refining the
194
242
  # name re-checks the reader.
195
- def refined?(name)
243
+ #
244
+ # Issue #2203 — for a call on `self` (`on_self:`) whose reach {#self_reach} knows, a row of the name counts only
245
+ # on a class key in that reach, on the wildcard class key, or on a key the walk could not name
246
+ # (`InEffectRefinements.class_wildcard_key?`); a names-wildcard row (a computed `define_method`,
247
+ # `import_methods`) counts as the name, but only on a class key in the reach. A refinement of `String` leaves
248
+ # a `Holder` alone: dropping the proof is no safer, since the intervening-call reset restores the class-ivar
249
+ # seed and a class guard's narrowing then reports. Where the reach is unknown, and for a call on another
250
+ # receiver, a row of the name counts on any class and a names-wildcard row never does, as before the issue.
251
+ def refined?(name, on_self: false)
252
+ wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
196
253
  if Analysis::DependencyRecorder.active?
197
254
  Analysis::DependencyRecorder.read_name(:refinement, name)
198
- Analysis::DependencyRecorder.read_name(:refinement, Scope::DiscoveryIndex::REFINEMENT_WILDCARD)
255
+ Analysis::DependencyRecorder.read_name(:refinement, wildcard)
199
256
  end
200
- @scope.discovered_refinements.any? { |_refined, methods| methods.key?(name) }
257
+ reach = on_self ? self_reach : nil
258
+ @scope.discovered_refinements.any? do |refined, methods|
259
+ next methods.key?(name) if reach.nil?
260
+
261
+ if reach.include?(refined)
262
+ methods.key?(name) || methods.key?(wildcard)
263
+ else
264
+ methods.key?(name) && InEffectRefinements.class_wildcard_key?(@scope, refined)
265
+ end
266
+ end
267
+ end
268
+
269
+ # Issue #2203 — the classes and modules on the instance chain of `self`'s class and of `Object`, `Kernel` and
270
+ # `BasicObject` (which holds the modules the project mixes into them), with `Object`'s RBS ancestors
271
+ # (`PP::ObjectMixin`); nil when a chain is cut, mixes in a module the tables cannot name, ends in a superclass
272
+ # they do not know (`class Holder < Struct.new(:a)`), or holds an external entry. A project subclass's chain
273
+ # needs no read: {#no_protocol_override} gives the proof up on a refined class or module anywhere on it. The
274
+ # implicit roots' chains are recorded, so a file that mixes a module into `Object` re-checks the reader;
275
+ # `self`'s own chain is recorded by {#no_protocol_override}'s read.
276
+ def self_reach
277
+ return @self_reach if defined?(@self_reach)
278
+
279
+ @self_reach = read_self_reach
280
+ end
281
+
282
+ def read_self_reach
283
+ names = Set.new
284
+ [@class_name.to_s, *IMPLICIT_ROOTS].each do |class_name|
285
+ chain = Scope::ResolutionChain.for(@scope, class_name, :instance, :methods)
286
+ chain.record(@scope) if Analysis::DependencyRecorder.active?
287
+ return nil if chain.truncated? || chain.wildcard_mixin? || chain.opaque_tail?(@scope)
288
+ return nil if chain.entries.any?(&:external?)
289
+
290
+ chain.entries.each { |entry| names << entry.name }
291
+ end
292
+ loader = @scope.environment&.rbs_loader
293
+ return nil if loader.nil?
294
+
295
+ names.merge(loader.ancestor_names_for("Object"))
201
296
  end
202
297
 
203
298
  def call_receiver(node) = node.is_a?(Prism::CallNode) ? node.receiver : nil
@@ -268,11 +363,7 @@ module Rigor
268
363
  end
269
364
 
270
365
  def definition_count(owner, name)
271
- count = 0
272
- @scope.discovered_deferred_ranges.each_value do |rows|
273
- count += rows.count { |row| row[2] == name && row[3] == :instance && row[4] == owner }
274
- end
275
- count
366
+ SelfCallIvarWrites.definition_counts(@scope.discovered_deferred_ranges).fetch([owner, name], 0)
276
367
  end
277
368
 
278
369
  # Whether `chain`'s own order stands with no second world: no order mark and no fork. `ResolutionChain#settle`
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "closure_escape_analyzer"
6
+ require_relative "project_method_ownership"
7
+ require_relative "refined_dispatch"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2861 — whether a block-carrying call provably runs its literal block under the CALLER's `self`, so the
12
+ # block body is no more opaque than the scope around it (`Scope#entering_self_preserving_block`).
13
+ #
14
+ # Issue #316 enters every block with `self` unmodelled, because a DSL call (`RSpec.describe`, `Class.new`, a
15
+ # project `def run(&b) = instance_exec(&b)`) may run the block on another object, and the call cannot be told
16
+ # apart from `Array#each` without knowing the callee. Here the callee is known: the explicit receiver's type and
17
+ # method are one {ClosureEscapeAnalyzer} classifies as `:non_escaping` — a catalogued core iterator, `tap` /
18
+ # `then`, or a project class's inherited `Enumerable` method — each of which `yield`s, which runs the block
19
+ # with its own `self`. The project must not define the method anywhere the receiver looks it up
20
+ # ({ProjectMethodOwnership.defines?}): a project reopening of `Array#each` could `instance_exec` the block.
21
+ # A `def` inside `refine X do … end` is no such answer, so where the call site's in-effect refinements may
22
+ # define the name the call declines too (issue #2909) — a refined `Integer#times` or `Object#tap` could
23
+ # `instance_exec` the block just the same.
24
+ #
25
+ # The project must not define it on `Object`, `Kernel` or another owner every receiver reaches either
26
+ # ({.universally_defined?}). A receiverless call is the DSL shape and never answers true, nor does a receiver Rigor
27
+ # cannot see. A `self.` receiver is read like any other explicit one, through its type.
28
+ module SelfPreservingBlock
29
+ module_function
30
+
31
+ # `classification` is the receiver's {ClosureEscapeAnalyzer.classify} answer when the caller already holds it.
32
+ def call?(call_node, receiver_type, scope, classification: nil)
33
+ return false unless call_node.is_a?(Prism::CallNode) && call_node.block.is_a?(Prism::BlockNode)
34
+
35
+ return false if call_node.receiver.nil? || receiver_type.nil?
36
+
37
+ method_name = call_node.name
38
+ classification ||= ClosureEscapeAnalyzer.classify(receiver_type: receiver_type, method_name: method_name,
39
+ scope: scope)
40
+ return false unless classification == :non_escaping
41
+
42
+ project_shadow_free?(call_node, receiver_type, method_name, scope)
43
+ rescue StandardError
44
+ false
45
+ end
46
+
47
+ # The project-side half of {.call?}: nothing the project defines — a plain `def` on a lookup target or a
48
+ # {UNIVERSAL_OWNERS} entry, nor a refinement in effect at the call site — may answer the yielding method
49
+ # instead of the catalogued non-escaping one.
50
+ def project_shadow_free?(call_node, receiver_type, method_name, scope)
51
+ targets = ProjectMethodOwnership.targets(receiver_type)
52
+ return false if targets.nil? || targets.empty?
53
+ return false if universally_defined?(method_name, scope)
54
+
55
+ targets.none? { |class_name, kind| ProjectMethodOwnership.defines?(class_name, method_name, kind, scope) } &&
56
+ !refined_in_effect?(call_node, method_name, scope)
57
+ end
58
+
59
+ # The classes every receiver's lookup reaches: an instance of any class is an `Object` (a class object is also
60
+ # a `Module` and a `Class`), so a project method of the name on one of them, or on `Kernel`, which
61
+ # {ProjectMethodOwnership.defines?} reaches through `Object`'s ancestry, may be the one the call runs, whatever
62
+ # class the receiver names. `ClosureEscapeAnalyzer` answers `tap` / `then` / `yield_self` for every class
63
+ # without asking who defines them.
64
+ UNIVERSAL_OWNERS = %w[Object Module Class].freeze
65
+ private_constant :UNIVERSAL_OWNERS
66
+
67
+ # Whether the project defines `method_name` on a {UNIVERSAL_OWNERS} entry. A top-level `def` of the name is
68
+ # private, so an explicit-receiver call raises NoMethodError before any block runs, and is not counted.
69
+ def universally_defined?(method_name, scope)
70
+ UNIVERSAL_OWNERS.any? { |owner| ProjectMethodOwnership.defines?(owner, method_name, :instance, scope) }
71
+ end
72
+
73
+ # Issue #2909 — whether a refinement in effect at `call_node` may define `method_name`. The refined-class
74
+ # walk is not consulted: any in-effect row for the name declines, the direction ADR-5 prefers, and it covers
75
+ # `refine Object` / `refine Kernel` without a universal-owner path of its own. An empty list — no `using`
76
+ # reaches the node — is the common case and costs the one lookup. The answer reads the refinement table by
77
+ # method name, so `refinement:<name>` is recorded as {ExpressionTyper}'s refined dispatch records it.
78
+ def refined_in_effect?(call_node, method_name, scope)
79
+ list = scope.in_effect_refinements(call_node)
80
+ return false if list.empty?
81
+
82
+ InEffectRefinements.record_refinement_names(method_name)
83
+ RefinedDispatch.refined_anywhere?(scope, method_name, list)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "content_join"
5
+ require_relative "mutation_rejoin"
6
+ require_relative "rewrite_mutation"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # The Set half of {MutationWidening}'s seams (issue #2294), beside {StringMutation}.
11
+ #
12
+ # `f = Set["a"]; f.clear; f << 1; f.map { |v| v.fdiv(2) }` reported `undefined method 'fdiv' for String`
13
+ # on code that prints `[0.5]`: the mutation seams answered `Tuple`, `HashShape`, `Constant[String]` and the
14
+ # re-openable `Array` / `Hash` nominals, so a `Set` kept the elements it was built with through every
15
+ # `<<`, `add`, `merge` and `replace`.
16
+ #
17
+ # A Set has no slots and no arity, so the only thing a mutation falsifies is the element set. Both carriers
18
+ # this module answers end at the typed Set #2290 builds for a non-constant literal — `Set[E | Dynamic[top]]`:
19
+ #
20
+ # - a constant-folded `Constant[Set]` widens to that carrier, its members' value pins dropped
21
+ # ({MethodDispatcher.array_new_element}) and joined with the `Dynamic[top]` floor;
22
+ # - a typed `Set` whose element already carries a gradual arm re-grows the way
23
+ # {MutationRejoin.regrowable_carrier?} lets an `Array` / `Hash` re-grow;
24
+ # - a PRECISE `Set[String]` (an RBS return such as `Array#to_set`) re-grows too (issue #2361), as a precise
25
+ # `Array[String]` does ({MutationRejoin.widen_precise_array}): `f = ARGV.to_set; f.clear; f << 1` is
26
+ # `Set[Dynamic[top] | String]`.
27
+ #
28
+ # A gradual seed admits every added member as itself: the seed-admissibility gate has nothing to contradict on
29
+ # it. A precise seed admits only a member its element accepts ({MutationRejoin.precise_admitted}), any other
30
+ # taking the floor, and takes no {RewriteMutation} arm: `replace` joins its elements onto the seed, and `map!`
31
+ # leaves it as it was. Every join carries the `Dynamic[top]` floor, as the Array re-join's does. An adder whose
32
+ # arguments the block-capture seam did not type joins only that floor: the store happened, and a Set has no
33
+ # block content join to add it afterwards. An adder called with no arguments changes nothing.
34
+ module SetMutation
35
+ # Receiver-mutating Set methods. The non-bang siblings (`map` vs `map!`, `select` vs `select!`) and the
36
+ # algebra (`|`, `+`, `-`, `&`, `^`) return a new Set and stay precise.
37
+ MUTATORS = %i[
38
+ << add add? merge replace
39
+ map! collect! flatten!
40
+ clear delete delete? delete_if keep_if select! filter! reject! subtract
41
+ ].to_set.freeze
42
+
43
+ # The {MUTATORS} that put elements in. `<<` / `add` / `add?` store their argument; `merge` stores every
44
+ # argument's elements; `replace` its argument's elements, after {RewriteMutation} has dropped the old ones.
45
+ CONTENT_ADDERS = %i[<< add add? merge replace].to_set.freeze
46
+
47
+ module_function
48
+
49
+ def constant?(type)
50
+ type.is_a?(Type::Constant) && type.value.is_a?(::Set)
51
+ end
52
+
53
+ # True when `type` is a `Set` carrier of either kind, which {#widen} answers for alone.
54
+ def set?(type)
55
+ constant?(type) || (type.is_a?(Type::Nominal) && type.class_name == "Set")
56
+ end
57
+
58
+ # True when `type` is a `Set` the seams may grow: a constant-folded one, or a typed one carrying a gradual
59
+ # element arm.
60
+ def carrier?(type)
61
+ constant?(type) || (type.is_a?(Type::Nominal) && regrowable?(type))
62
+ end
63
+
64
+ # True when `type`, or a member of it, is a {#carrier?}: the only types a Set-only adder's floor can change.
65
+ def carrier_in?(type)
66
+ type.is_a?(Type::Union) ? type.members.any? { |member| carrier?(member) } : carrier?(type)
67
+ end
68
+
69
+ # Every typed `Set[E]`, a precise one included since issue #2361.
70
+ def regrowable?(nominal)
71
+ nominal.class_name == "Set" && nominal.type_args.size == 1
72
+ end
73
+
74
+ # The carrier `type` holds after `method_name` with argument types `arg_types`, or `nil` when the call is not
75
+ # a Set mutator or `type` is not a carrier this seam grows. `nil` also answers a re-join that changed nothing,
76
+ # so a remover on an already-widened Set does not rebind it. That is a guard, not a behaviour: a Set has no
77
+ # `receiver[key] ||=` slot narrowings for a rebind to drop, so the identical carrier would read the same either
78
+ # way, and declining only saves rebuilding the scope.
79
+ #
80
+ # Issue #2555 L3 — `synthesised` says the binding carries the `:synthesised` construction fact: the engine built
81
+ # the Set itself (`s = Set[1]`, `Set.new(...)`), so no declaration made a claim about its elements, and the
82
+ # carrier stays re-growable for as long as the binding keeps the fact. Such a Set joins without the floor
83
+ # ({#floored}'s and {#join}'s) and without the precise seed's admissibility gate: `s = Set[1]; s << 2` reads
84
+ # `Set[Integer]`, and a read of its member reports what an Integer does not answer.
85
+ def widen(type, method_name, arg_types, unread_arguments: false, synthesised: false)
86
+ return nil unless MUTATORS.include?(method_name)
87
+
88
+ if constant?(type)
89
+ members = type.value.map { |v| Type::Combinator.constant_of(v) }
90
+ seed = synthesised && !members.empty? ? exact(members) : floored(members)
91
+ join(seed, method_name, arg_types, unread_arguments, synthesised:)
92
+ elsif type.is_a?(Type::Nominal) && regrowable?(type)
93
+ joined = join(type, method_name, arg_types, unread_arguments, synthesised:)
94
+ joined == type ? nil : joined
95
+ end
96
+ end
97
+
98
+ # Every join carries the `Dynamic[top]` floor, a precise seed's included (issue #2361): a later remover or
99
+ # rewriter (`subtract`, `delete`, `map!`) can take the stored members out again, and nothing narrows the
100
+ # carrier back, so a closed `Set[Integer | String]` would reject a declared `Set[String]` the program meets.
101
+ # A synthesised carrier (issue #2555) takes the rewrite arm a gradual one takes, admits every added member, and
102
+ # joins no floor: an untyped store joins `Dynamic[top]` as the one unknown member it is.
103
+ def join(carrier, method_name, arg_types, unread_arguments, synthesised: false)
104
+ seed = carrier.type_args.first
105
+ # A precise seed takes no rewrite arm, as a precise `Array` does not ({MutationRejoin.widen_precise_array}).
106
+ carrier = RewriteMutation.arm(carrier, method_name) if synthesised || MutationRejoin.gradual_parameter?(seed)
107
+ return carrier unless CONTENT_ADDERS.include?(method_name)
108
+
109
+ stored = added_elements(method_name, arg_types)
110
+ if stored.empty?
111
+ # An adder called with no arguments (`s.merge`) is a no-op; one whose arguments went untyped is a store.
112
+ return carrier unless unread_arguments
113
+
114
+ added = []
115
+ else
116
+ return carrier if !MutationRejoin.gradual_parameter?(seed) && MutationRejoin.already_held?(seed, stored)
117
+
118
+ added = admitted(seed, stored.map { |t| widened_member(t) }, synthesised:)
119
+ end
120
+ floor = synthesised && !added.empty? ? [] : [Type::Combinator.untyped]
121
+ element = Type::Combinator.union(carrier.type_args.first, *added, *floor)
122
+ Type::Combinator.nominal_of("Set", type_args: [element])
123
+ end
124
+
125
+ # `added` as {MutationRejoin.precise_admitted} admits it into a precise seed; a gradual or synthesised seed
126
+ # admits all.
127
+ def admitted(seed, added, synthesised: false)
128
+ return added if synthesised || MutationRejoin.gradual_parameter?(seed)
129
+
130
+ MutationRejoin.precise_admitted(seed, added)
131
+ end
132
+
133
+ # The element types one content-adder call puts in, from the call's argument types.
134
+ def added_elements(method_name, arg_types)
135
+ return [] if arg_types.empty?
136
+
137
+ case method_name
138
+ when :merge, :replace then arg_types.flat_map { |t| element_types(t) }
139
+ else [arg_types.first || Type::Combinator.untyped]
140
+ end
141
+ end
142
+
143
+ # `Set[E]` over `members`, value pins dropped: a constant Set a synthesised binding holds (issue #2555).
144
+ def exact(members)
145
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(*members.map { |m| widened_member(m) })])
146
+ end
147
+
148
+ # `Set[E | Dynamic[top]]` over `members`, the #2290 typed-Set floor. An empty seed (`Set.new`) is
149
+ # `Set[Dynamic[top]]`.
150
+ def floored(members)
151
+ widened = members.map { |m| widened_member(m) }
152
+ Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(*widened, Type::Combinator.untyped)])
153
+ end
154
+
155
+ # The element types an enumerable argument (`merge`'s, `replace`'s, the algebra's `_Each[A]`) yields: a
156
+ # `Tuple`'s elements, an `Array` / `Set` / `Range` nominal's element parameter, a constant Set's or Range's
157
+ # members. Anything else yields elements this seam cannot name, which are unknown rather than none.
158
+ def element_types(type)
159
+ case type
160
+ when Type::Union then type.members.flat_map { |m| element_types(m) }
161
+ when Type::Tuple then type.elements
162
+ when Type::Nominal
163
+ if %w[Array Set Range].include?(type.class_name) && type.type_args.size == 1
164
+ type.type_args
165
+ else
166
+ [Type::Combinator.untyped]
167
+ end
168
+ when Type::Constant then constant_elements(type)
169
+ else [Type::Combinator.untyped]
170
+ end
171
+ end
172
+
173
+ def constant_elements(constant)
174
+ value = constant.value
175
+ if value.is_a?(::Set) then value.map { |v| Type::Combinator.constant_of(v) }
176
+ elsif value.is_a?(::Range) && value.begin.is_a?(Integer)
177
+ [Type::Combinator.nominal_of("Integer")]
178
+ else [Type::Combinator.untyped]
179
+ end
180
+ end
181
+
182
+ # A stored member's value pin and literal shape are dropped, as an `Array.new` fill's are: the set holds the
183
+ # object, and the program keeps a reference to it.
184
+ def widened_member(type)
185
+ MethodDispatcher.array_new_element(type)
186
+ end
187
+ end
188
+ end
189
+ end
@@ -37,7 +37,8 @@ module Rigor
37
37
  # Known remainders, pinned in `spec/integration/definer_resolution_witness_spec.rb`: `class << self; prepend
38
38
  # P` is recorded as an `extend` (the chain places `P` after the singleton, so an own `def self.x` is trusted
39
39
  # against it); a concern is capable for every name; an own `def self.x` is trusted against its own level's
40
- # U2 hooks and same-class redefinitions; a `class Class; def inherited` monkeypatch is unseen; and a
40
+ # U2 hooks and same-class redefinitions, except a concern block's `def self.x` of the same name (#3005, which
41
+ # the read tests); a `class Class; def inherited` monkeypatch is unseen; and a
41
42
  # superclass that only `extend`s adds two forks, so every read on its subclasses declines.
42
43
  module SingletonHookDecline
43
44
  WILDCARD = "*"
@@ -65,7 +66,8 @@ module Rigor
65
66
  entry = chain.entries[index]
66
67
  # A singleton project entry on a singleton chain is always a level's class entry.
67
68
  own = !entry.external? && entry.side == :singleton && def_head(scope, entry.name, method_name) == entry.name
68
- own ? index : index + 1
69
+ # Issue #3005 — an own `def self.x` that a concern block's `def self.x` on its level may replace is tested.
70
+ own && !chain.hook_block_rival?(scope, index, method_name) ? index : index + 1
69
71
  end
70
72
 
71
73
  # What a class entry's answer is when its `def` was written in another module's body: `:copy` when that
@@ -92,21 +94,28 @@ module Rigor
92
94
  # Whether the class's singleton `def` is the very node `head`'s instance table holds, the one the fold
93
95
  # copied: a receiverless `def`. A `def C.x` or `class << C; def x; end` written inside `head`'s body has the
94
96
  # same nesting head and is the class's own, not a copy. (The walk records a `def C.x` written in `module X`
95
- # as `X#x` too, so identity alone does not tell it from a copy; its receiver does.)
97
+ # as `X#x` too, so identity alone does not tell it from a copy; its receiver does.) Issue #3155 — the walk no
98
+ # longer files that `def C.x` anywhere, so the copy is all the table holds; the definer's mark on the class
99
+ # (`ENVELOPE_OBJECT_SINGLETON_KIND`, {ScopeIndexer#record_object_singleton}) still says the class has its own.
96
100
  def copied_from?(scope, head, method_name, owner)
97
101
  node = scope.singleton_def_for(owner, method_name)
98
102
  return false if node.nil? || !node.receiver.nil?
103
+ return false if foreign_singleton_definer?(scope, owner, method_name)
99
104
 
100
105
  scope.user_def_for(head, method_name).equal?(node)
101
106
  end
102
107
 
108
+ def foreign_singleton_definer?(scope, owner, method_name)
109
+ key = [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name.to_sym]
110
+ scope.parameter_envelopes_of(owner).key?(key)
111
+ end
112
+
103
113
  # The innermost `Module.nesting` entry `owner`'s singleton `def` of the name was written in, or nil.
104
114
  def def_head(scope, owner, method_name)
105
115
  node = scope.singleton_def_for(owner, method_name)
106
116
  return nil if node.nil?
107
117
 
108
- nesting = scope.discovery.discovered_def_nestings[node] || DefNodeResolver.rehydrated_nesting(node)
109
- nesting&.first
118
+ scope.discovery.recorded_def_nesting(node)&.first
110
119
  end
111
120
 
112
121
  # Issue #1722 — the hook-capable test for one chain entry, asked by `call.undefined-method` of a project module
@@ -171,15 +180,40 @@ module Rigor
171
180
 
172
181
  # The memoised `[verdict, edges]` of one test, its edges replayed while a recording is active.
173
182
  def verdict(kind, key)
174
- verdict, edges = @memo[[kind, key]] ||= begin
183
+ memo_key = [kind, key]
184
+ entry = @memo[memo_key]
185
+ if entry.nil?
175
186
  edges = []
176
- value = yield(edges)
177
- [value ? true : false, edges.concat(own_edges(kind, key)).uniq.freeze].freeze
187
+ value = MemoAudit.key_audit("singleton hook verdict") { yield(edges) }
188
+ entry = @memo[memo_key] = [value ? true : false, edges.concat(own_edges(kind, key)).uniq.freeze].freeze
189
+ else
190
+ entry = MemoAudit.check("singleton hook verdict", entry, kind, key) { fresh_verdict(kind, key) }
178
191
  end
192
+ verdict, edges = entry
179
193
  replay(edges) if @recording
180
194
  verdict
181
195
  end
182
196
 
197
+ # ADR-129 G2 — a memoised verdict rebuilt for the audit by a new Context, whose memo is a fresh bucket's
198
+ # while the recomputation runs ({Scope::ResolutionChain.hook_memo}), its dependency reads withheld.
199
+ def fresh_verdict(kind, key)
200
+ Analysis::DependencyRecorder.withhold { Context.new(@scope, @flavor).verdict_entry(kind, key) }.first
201
+ end
202
+
203
+ protected
204
+
205
+ # The `[verdict, edges]` entry of one test, computed through {#verdict} on this Context's memo.
206
+ def verdict_entry(kind, key)
207
+ case kind
208
+ when :project then verdict(:project, key) { project_capable?(key) }
209
+ when :external then verdict(:external, key) { external_capable?(key) }
210
+ else instance_level_capable?(key)
211
+ end
212
+ @memo[[kind, key]]
213
+ end
214
+
215
+ private
216
+
183
217
  def own_edges(kind, key)
184
218
  return entry_edges(key) if kind == :external
185
219
  return EMPTY_EDGES if kind == :instance_level
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../source/constant_path"
4
+ require_relative "../source/parameter_envelope"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -45,18 +46,47 @@ module Rigor
45
46
  name = receiver_constant_name(call_node, receiver_type)
46
47
  return false if name.nil?
47
48
 
48
- scope.discovered_method?(name, method_name, :singleton)
49
+ scope.discovered_method?(name, method_name, :singleton) || foreign_singleton?(scope, name, method_name)
50
+ end
51
+
52
+ # Issue #3016 — `def Const.x` written outside `Const`'s own body, and `Const.define_singleton_method(:x)`, record
53
+ # no `:singleton` row: {ScopeIndexer} files them as an opaque `[:singleton, x]` envelope on every name the
54
+ # constant can denote from the def site. That envelope is the evidence the method exists on the constant's
55
+ # object. The read records the constant's class edge, and that edge's file set holds every file writing such a
56
+ # def or call, so adding or removing one re-checks the call.
57
+ #
58
+ # A computed `Const.define_singleton_method(name)` may define any name, so the singleton side's open-names mark it
59
+ # files declines every call through the constant.
60
+ def foreign_singleton?(scope, name, method_name)
61
+ bucket = scope.parameter_envelopes_of(name)
62
+ bucket.key?([:singleton, method_name.to_sym]) ||
63
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
49
64
  end
50
65
 
51
66
  # The `Prism::DefNode` for `method_name` in the constant's singleton body, or nil. The inference tier's
52
67
  # entry point.
68
+ #
69
+ # Issue #3018 — nil too when another definer of the name on the constant may replace that def: a `def Const.x`,
70
+ # a `Const.define_singleton_method(:x)` or a `singleton_class.class_eval` def joins an opaque envelope into the
71
+ # name's `[:singleton, x]` slot, and a computed `define_singleton_method` files the open-names mark. The body
72
+ # def's own contribution is its parameter envelope, never opaque, so a lone body def keeps its typed answer.
73
+ # Declined whatever the source order, since a two-file project has none.
53
74
  def def_node_for(call_node, receiver_type, method_name, scope)
54
75
  return nil if scope.nil?
55
76
 
56
77
  name = receiver_constant_name(call_node, receiver_type)
57
78
  return nil if name.nil?
58
79
 
59
- scope.singleton_def_for(name, method_name)
80
+ def_node = scope.singleton_def_for(name, method_name)
81
+ return nil if def_node.nil? || replaceable?(scope, name, method_name)
82
+
83
+ def_node
84
+ end
85
+
86
+ def replaceable?(scope, name, method_name)
87
+ bucket = scope.parameter_envelopes_of(name)
88
+ Source::ParameterEnvelope.opaque?(bucket[[:singleton, method_name.to_sym]]) ||
89
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK)
60
90
  end
61
91
  end
62
92
  end