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
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../typing_state"
4
+ require_relative "../memo_audit"
5
+
3
6
  module Rigor
4
7
  class Scope
5
8
  # ADR-24 (amended for #1567, #1568, #1570 and #1571) — Ruby's method resolution order for one project class or
@@ -90,6 +93,39 @@ module Rigor
90
93
  # when the entry is interned.
91
94
  Entry = Data.define(:name, :side, :raw, :candidates, :superclass_edge, :last_segment, :owner) do
92
95
  def external? = name.nil?
96
+
97
+ # Issue #3156 — the BODY-LESS class or module the project declares that an external entry names, or nil: the
98
+ # first of its spellings that is a `discovered_classes` key, read in Ruby's lookup order. Such a declaration
99
+ # records no method, superclass or include (`module PMod; end`), which is why the chain holds it as external.
100
+ # The owner-qualified spelling (#1698) leads only on a mixin edge: Ruby evaluates a superclass expression
101
+ # outside the class's own body, so `class Leaf < Base` never names `Leaf::Base`. Any spelling up to the match
102
+ # that RBS alone knows, that a project constant write binds (`AMid = ::Other` in `NS`), or that names a class
103
+ # with a body leaves nil, since Ruby may resolve the constant there or the chain should have held it. It files
104
+ # the entry's negative class and constant edges, which a declaration or write of the name appearing or
105
+ # vanishing fires.
106
+ def declared_name(scope)
107
+ spelling = raw.to_s
108
+ Analysis::DependencyRecorder.read_last_segment(:class, spelling)
109
+ Analysis::DependencyRecorder.read_last_segment(:constant, spelling)
110
+ own = owner && !superclass_edge && !spelling.start_with?("::")
111
+ (own ? ["#{owner}::#{spelling}", *candidates] : candidates).uniq.each do |candidate|
112
+ return nil if constant_written?(scope, candidate)
113
+ return (scope.known_user_class?(candidate) ? nil : candidate) if scope.discovered_classes.key?(candidate)
114
+ return nil if Rigor::Reflection.rbs_class_known?(candidate, scope: scope)
115
+ end
116
+ nil
117
+ end
118
+
119
+ private
120
+
121
+ # Whether a project constant write other than a class declaration binds `name` exactly (the census keeps a
122
+ # write as spelled, a `*::` wildcard for one it cannot place), or a literal `const_set` in this file does
123
+ # (`LastLine::SelfEvidence#const_set_spellings`; the census files none).
124
+ def constant_written?(scope, name)
125
+ (scope.bound_constant_names(name) + scope.shadowing_constant_names(name)).any? do |written|
126
+ written == name || written.start_with?("*::")
127
+ end || scope.discovery.implicit_self_evidence&.const_set_spellings&.include?(name) || false
128
+ end
93
129
  end
94
130
 
95
131
  # How many project entries a chain may hold before it is cut (ADR-41 WD4). The breadth-first walks
@@ -102,9 +138,20 @@ module Rigor
102
138
  # the multi-file rule fired (two or more files declare it and it has two or more edges on that side).
103
139
  # Interned per `[node, kind]` in the flavor's bucket, so equal marks are one object. Kind `:extend_hook`
104
140
  # (#1730, listing `"*"`) marks a node a module it extends may mix into through a hook; it is not an order mark
105
- # ({#order_marks}).
141
+ # ({#order_marks}). Kind `:include_hook` (#1592, listing `"*"` and `"?"`) marks a class object a module it
142
+ # includes or prepends may extend, through a hook, with a module no table names; nor is it. Kind `:hook_order`
143
+ # (#1592, listing nothing) marks a class object whose own `extend`s and hook-extended modules have no known
144
+ # order between them: the fork it counts already settles the chain, and the mark tells the typing read why.
145
+ # Kind `:singleton_prepend` (#2998, listing nothing) marks a class object with singleton-side prepends of its
146
+ # own, which master's order does not place, and kind `:prepend_order` (#2998, listing nothing) one whose
147
+ # singleton-side prepends have no known order among themselves; neither is an order mark. Kind `:unnamed_prepend`
148
+ # (#3020, listing nothing) marks a class object with a singleton-side prepend of a module no table names, which
149
+ # is not an order mark either.
106
150
  Mark = Data.define(:node, :kind, :listed, :multi_file)
107
151
 
152
+ # The mark kinds that are not order marks.
153
+ HOOK_MARK_KINDS = %i[extend_hook include_hook hook_order singleton_prepend prepend_order unnamed_prepend].freeze
154
+
108
155
  NO_MARKS = [].freeze
109
156
 
110
157
  attr_reader :root, :side, :entries, :level_starts, :level_classes, :flavor
@@ -131,6 +178,13 @@ module Rigor
131
178
  # "no definer" from "a definer past the cut", which is budget uncertainty, not absence.
132
179
  def truncated? = @truncated
133
180
 
181
+ # ADR-129 G2 — the chain's content, for the memo audit: a recomputation builds a new chain object, which
182
+ # equals the stored one field for field (its retro world included) when the memo's key holds every input.
183
+ def audit_signature
184
+ [@root, @side, @entries, @level_starts, @level_classes, @levels_end, @truncated, @forks, @marks, @flavor,
185
+ @retro&.audit_signature]
186
+ end
187
+
134
188
  # How many forks the replay met while this chain was built, counted across every module sub-chain it drew
135
189
  # on (a memoised sub-chain hands its own count up): each entry that reached the chain by a second route.
136
190
  # An over-count only ever sends a reader to master's answer.
@@ -147,13 +201,57 @@ module Rigor
147
201
  # those readers to master's order would answer a constant lookup breadth-first, which is wrong for the
148
202
  # #1567 shape on every class that extends such a module.
149
203
  def order_marks
150
- @memo[:order_marks] ||= @marks.reject { |mark| mark.kind == :extend_hook }.freeze
204
+ @memo[:order_marks] ||= @marks.reject { |mark| HOOK_MARK_KINDS.include?(mark.kind) }.freeze
151
205
  end
152
206
 
153
207
  # True when a class on the chain records a mixin the tables cannot name (`"*"`: an `include` of a
154
208
  # non-constant, or a call the walk cannot record), so the chain may hold a module it does not list.
155
209
  def wildcard_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::WILDCARD) }
156
210
 
211
+ # Issue #2991 — true when a class on the chain mixes in a module the tables do not record under any name
212
+ # (`include(*MODS)`, `include helper`, a hook's `base.include M`, an unrecorded `self.include M`): the chain's SET
213
+ # of modules is unknown, not only its order. Every such mark is also a {#wildcard_mixin?} one; `include M;
214
+ # prepend M` taints the order alone.
215
+ def unnamed_mixin? = @marks.any? { |mark| mark.listed.include?(Relevance::UNNAMED) }
216
+
217
+ # Issue #1592 — whether a class object on the chain has hook-extended modules whose order against its own
218
+ # `extend`s, or among themselves, is not a fact ({Builder#singleton_mixins}).
219
+ def hook_order_marked? = @marks.any? { |mark| mark.kind == :hook_order }
220
+
221
+ # Issue #2998 — whether a class object on the chain has singleton-side prepends of its own
222
+ # ({Builder#own_singleton_prepends}), which master's order does not place.
223
+ def singleton_prepend_marked? = @marks.any? { |mark| mark.kind == :singleton_prepend }
224
+
225
+ # Issue #2998 — the class objects on the chain whose prepend region has no known order
226
+ # ({Builder#order_prepends}).
227
+ def prepend_order_nodes
228
+ @memo[:prepend_order_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :prepend_order }.freeze
229
+ end
230
+
231
+ # Issue #3020 — the class objects on the chain with a singleton-side prepend of a module no table names
232
+ # ({Builder#own_singleton_prepends}).
233
+ def unnamed_prepend_nodes
234
+ @memo[:unnamed_prepend_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :unnamed_prepend }.freeze
235
+ end
236
+
237
+ # Issue #3020 — the class objects on the chain whose own singleton side lists a module no table names
238
+ # (`extend(*MODS)`, `class << self; include(*MODS)`, `singleton_class.prepend(*MODS)`: the `:extend` order mark
239
+ # listing {Relevance::UNNAMED}).
240
+ def unnamed_extend_nodes
241
+ @memo[:unnamed_extend_nodes] ||= @marks.filter_map do |mark|
242
+ mark.node if mark.kind == :extend && mark.listed.include?(Relevance::UNNAMED)
243
+ end.freeze
244
+ end
245
+
246
+ # The chain's retro world ({#settle}), or nil when it has none.
247
+ def retro_world = @retro
248
+
249
+ # Issue #1592 — the class objects on a singleton chain that a module they include or prepend may extend, through
250
+ # a hook, with a module no table names: that module sits right after the class object's own entry.
251
+ def hook_open_nodes
252
+ @memo[:hook_open_nodes] ||= @marks.filter_map { |mark| mark.node if mark.kind == :include_hook }.freeze
253
+ end
254
+
157
255
  # The ONE decision every first-definer reader makes: does `answer`, read off this chain, stand, or does
158
256
  # the reader answer what the walk this chain replaced answered ({MasterOrder})? Returns `:chain` or
159
257
  # `:master`.
@@ -222,18 +320,30 @@ module Rigor
222
320
  # it does not head the part searched (its prepends, or its own edges when the search starts past it,
223
321
  # placed the rest). The entry that answers records none of its own: its method edge is the caller's
224
322
  # `Scope#user_def_for`. A read that finds nothing has passed every entry.
323
+ #
324
+ # Issue #2998 — on a singleton chain a NEW file reopening a class object's class can prepend to its singleton
325
+ # (`class << self; prepend P`), ahead of everything the class object's level holds, which no class edge of the
326
+ # declaring files sees. So each class object the search passes, and one that answers, also files the name edge
327
+ # `mark:<last segment>` ({#record_prepend_regions}), which `IncrementalSession#mark_affected` fires when a
328
+ # `[ENVELOPE_SINGLETON_PREPEND_KIND, …]` entry (a diffed class mark) appears on or vanishes from the class, and
329
+ # not on a plain reopening. A class object behind the answer is filed through {#settle}'s filing, and so is every
330
+ # one on a read that finds nothing.
225
331
  def search(scope, start = 0, stop = nil, side: nil)
226
332
  recording = Analysis::DependencyRecorder.active?
227
333
  stop ||= @entries.size
228
334
  if recording
229
335
  record_head(scope, start, side)
230
336
  record_extend_hooks(scope)
337
+ record_include_hooks(scope)
231
338
  end
232
339
  index = start
233
340
  while index < stop
234
341
  entry = @entries[index]
235
342
  answer = side.nil? || entry.side == side ? yield(entry) : nil
236
- return answer if answer
343
+ if answer
344
+ record_prepend_regions(start, index) if recording
345
+ return answer
346
+ end
237
347
 
238
348
  ResolutionChain.record_entry(scope, entry, side) if recording
239
349
  index += 1
@@ -241,6 +351,19 @@ module Rigor
241
351
  nil
242
352
  end
243
353
 
354
+ # The `mark:` name edges of the class objects on a singleton chain from `start` through `last`.
355
+ def record_prepend_regions(start, last)
356
+ return unless @side == :singleton
357
+
358
+ (start..last).each do |index|
359
+ entry = @entries[index]
360
+ next unless entry.side == :singleton && !entry.external?
361
+
362
+ Analysis::DependencyRecorder.read_last_segment(:mark, entry.name)
363
+ end
364
+ end
365
+ private :record_prepend_regions
366
+
244
367
  # Records the ADR-46 class edge of the root and of every project entry ahead of `stop` (on `side`, when
245
368
  # given; every entry when `stop` is nil) — the dependency of a read that consulted those positions.
246
369
  def record(scope, stop = nil, side: nil)
@@ -249,6 +372,21 @@ module Rigor
249
372
  ResolutionChain.record_class(scope, @root)
250
373
  (stop || @entries.size).times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
251
374
  record_extend_hooks(scope)
375
+ record_include_hooks(scope)
376
+ end
377
+
378
+ # Issue #2689 — the negative edge a method-walk MISS owes each EXTERNAL entry: `class:<last segment>` of the name
379
+ # as written. An external entry is an ancestor the project declares with no methods (the usual empty
380
+ # `ApplicationRecord`) or not at all yet, so the walk looked nothing up there; a later file that declares it
381
+ # with the method turns the miss, and the declaration appearing is the one event that file fires. A core or gem
382
+ # ancestor (`StandardError`, `Comparable`) files its key too, which fires only on a file that declares a
383
+ # constant of that name. Recorded only on a miss: a hit's answer cannot be turned by an entry gaining a method.
384
+ def record_external_absence
385
+ return unless Analysis::DependencyRecorder.active?
386
+
387
+ @entries.each do |entry|
388
+ Analysis::DependencyRecorder.read_last_segment(:class, entry.raw.to_s) if entry.external?
389
+ end
252
390
  end
253
391
 
254
392
  # What a {#settle} verdict adds to what the search already filed. The search files the root and every entry
@@ -277,6 +415,7 @@ module Rigor
277
415
  Analysis::DependencyRecorder.file_chain_once(self, start, scope.discovery) { file_beyond(scope, start) }
278
416
  record_hooks(scope)
279
417
  record_extend_hooks(scope)
418
+ record_include_hooks(scope)
280
419
  end
281
420
  private :record_beyond
282
421
 
@@ -323,7 +462,7 @@ module Rigor
323
462
  end
324
463
  private :record_hooks
325
464
 
326
- # #1730 — the edges of the {Builder#extend_hook_taint?} decision: an instance chain's project entries may carry
465
+ # #1730 — the edges of the {Builder#extend_hook_taint} decision: an instance chain's project entries may carry
327
466
  # a `"*"` mark because a module one of them `extend`s has a hook that mixes into the extender, and the
328
467
  # module is often declared in a file the chain records nothing of. Each such module the decision read files
329
468
  # its class edges (an edit to its hook moves its declaration signature) and the negative method edges of
@@ -350,6 +489,168 @@ module Rigor
350
489
  end
351
490
  private :record_extend_hooks
352
491
 
492
+ # #1592 — the edges of the {Builder#hook_extensions} read: a singleton chain draws, for every class on it, the
493
+ # modules that class's direct mixins' hooks extend it with, and the mixins are declared in files the chain's own
494
+ # entries record nothing of. Each resolved mixin files its class edges and the negative `mark:` edge on its name,
495
+ # which an edit adding or dropping a hook-extend entry fires; every mixin, and every module a hook extends by
496
+ # name, files the negative class edge on its last segment, which a NEW file declaring or reopening it fires.
497
+ # Filed once per consumer and chain.
498
+ def record_include_hooks(scope)
499
+ return unless @side == :singleton
500
+
501
+ edges = (@memo[:include_hook_edges] ||= ResolutionChain.include_hook_edges(scope, @flavor, @level_classes))
502
+ return if edges.empty?
503
+
504
+ Analysis::DependencyRecorder.file_chain_once(self, :include_hooks, scope.discovery) do
505
+ edges.each do |kind, name|
506
+ ResolutionChain.record_class(scope, name) if kind == :mixin
507
+ Analysis::DependencyRecorder.read_last_segment(:mark, name) if kind == :mixin
508
+ Analysis::DependencyRecorder.read_last_segment(:class, name)
509
+ end
510
+ end
511
+ end
512
+ private :record_include_hooks
513
+
514
+ # `[[:mixin, resolved name] | [:spelling, raw], …]`: the direct mixins of every class on a singleton chain, and
515
+ # the modules the resolved ones' hooks extend by name ({Builder#hook_extensions}).
516
+ def self.include_hook_edges(scope, flavor, classes)
517
+ resolver = resolver_for(scope, flavor)
518
+ discovery = scope.discovery
519
+ classes.each_with_object([]) do |klass, out|
520
+ next if klass.nil?
521
+
522
+ (discovery.discovered_includes[klass] || EMPTY_NAMES).uniq.each do |raw|
523
+ mixin = resolver.resolve_declared(klass, raw)
524
+ next out << [:spelling, raw].freeze if mixin.nil?
525
+
526
+ out << [:mixin, mixin].freeze
527
+ hook_extend_keys(discovery, mixin).each { |key| out << [:spelling, key[1]].freeze }
528
+ concern_dependency_edges(discovery, resolver, mixin, out)
529
+ end
530
+ end.uniq.freeze
531
+ end
532
+
533
+ # Issue #2992 — the edges of a concern mixin's dependency read ({.concern_dependencies}): every module its
534
+ # dependency entries name, a concern or not yet one (a file that adds `extend ActiveSupport::Concern` to it
535
+ # turns it into one), as a mixin, with the modules its hooks extend by name; a spelling no module answers files
536
+ # its negative class edge. A concern among them is followed further.
537
+ def self.concern_dependency_edges(discovery, resolver, mixin, out, seen = [mixin])
538
+ return unless concern_bucket?(discovery, mixin)
539
+
540
+ dependency_spellings(discovery, mixin).each do |raw|
541
+ dependency = resolver.resolve_declared(mixin, raw)
542
+ next out << [:spelling, raw].freeze if dependency.nil?
543
+ next if seen.include?(dependency)
544
+
545
+ seen << dependency
546
+ out << [:mixin, dependency].freeze
547
+ hook_extend_keys(discovery, dependency).each { |key| out << [:spelling, key[1]].freeze }
548
+ concern_dependency_edges(discovery, resolver, dependency, out, seen)
549
+ end
550
+ end
551
+ private_class_method :concern_dependency_edges
552
+
553
+ # The `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` spellings of a module's bucket, nearest first (a later
554
+ # statement's ahead of an earlier one's).
555
+ def self.dependency_spellings(discovery, name)
556
+ bucket = discovery.discovered_parameter_envelopes[name]
557
+ return EMPTY_NAMES if bucket.nil?
558
+
559
+ bucket.each_key.filter_map do |key|
560
+ key[1] if key.is_a?(Array) && key.first == DiscoveryIndex::ENVELOPE_CONCERN_DEPENDENCY_KIND
561
+ end.reverse.uniq
562
+ end
563
+
564
+ # Issue #2992 — the concerns a concern `mixin` provably takes as dependencies (its
565
+ # `[ENVELOPE_CONCERN_DEPENDENCY_KIND, written]` entries: an `include` or `prepend` after its own `extend
566
+ # ActiveSupport::Concern`), at any depth through concerns, nearest first, without `mixin`:
567
+ # `Concern#append_features` records each as a dependency of `mixin` and includes it into `mixin`'s final
568
+ # includer, so the includer's singleton chain gains what each one's hooks extend it with. Empty when `mixin` is
569
+ # no concern. A dependency the walk meets twice is listed once, at its nearest place.
570
+ def self.concern_dependencies(discovery, resolver, mixin, seen = [mixin])
571
+ return EMPTY_NAMES unless concern_bucket?(discovery, mixin)
572
+
573
+ out = []
574
+ dependency_spellings(discovery, mixin).each do |raw|
575
+ dependency = resolver.resolve_declared(mixin, raw)
576
+ next if dependency.nil? || seen.include?(dependency) || !concern_bucket?(discovery, dependency)
577
+
578
+ seen << dependency
579
+ out << dependency
580
+ out.concat(concern_dependencies(discovery, resolver, dependency, seen))
581
+ end
582
+ out
583
+ end
584
+
585
+ # The entry an `extend ActiveSupport::Concern` module records ({Inference::ScopeIndexer}).
586
+ CONCERN_KEY = [DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND, "ClassMethods", :const_get].freeze
587
+ private_constant :CONCERN_KEY
588
+
589
+ def self.concern_bucket?(discovery, name)
590
+ bucket = discovery.discovered_parameter_envelopes[name]
591
+ !bucket.nil? && bucket.key?(CONCERN_KEY)
592
+ end
593
+ private_class_method :concern_bucket?
594
+
595
+ # Issue #2997 — whether `owner.const_get(raw)` may find the project's `candidate` (`"#{owner}::#{raw}"`): a
596
+ # `class_methods do … end` block's `def`s are filed on `Owner::ClassMethods` in every body of the module, but
597
+ # Ruby has that module only when `owner` is a concern, so one that no `module` declaration writes counts only
598
+ # for a concern `owner`.
599
+ def self.const_get_candidate?(discovery, owner, candidate)
600
+ concern_bucket?(discovery, owner) || discovery.discovered_classes.key?(candidate)
601
+ end
602
+
603
+ # Issue #3005 — whether `name` is a module a concern block's `def self.x`s are filed on (`Owner::<included>`).
604
+ def self.hook_block_module?(name)
605
+ DiscoveryIndex::HOOK_BLOCK_SEGMENTS.any? { |segment| name.end_with?("::#{segment}") }
606
+ end
607
+
608
+ # Issue #3005 — whether a hook-block module on the level of the class object at `index` defines `method_name`:
609
+ # Ruby defines it on that class object itself when the block runs, so it and the class's own `def self.x`
610
+ # replace each other in an order the tables do not hold (the `include` against the `def`).
611
+ def hook_block_rival?(scope, index, method_name)
612
+ level = @level_starts.rindex { |start| start <= index }
613
+ return false if level.nil? || level >= level_count
614
+
615
+ level_entries(level).any? do |entry|
616
+ !entry.external? && entry.side == :instance && ResolutionChain.hook_block_module?(entry.name) &&
617
+ !scope.user_def_for(entry.name, method_name).nil?
618
+ end
619
+ end
620
+
621
+ GATED_HOOKS = DiscoveryIndex::CONCERN_BLOCK_HOOKS.invert.freeze
622
+ private_constant :GATED_HOOKS
623
+
624
+ # Issue #3005 — the hook an entry of `owner`'s bucket naming `hook` runs on: a gated concern-block hook
625
+ # ({DiscoveryIndex::CONCERN_BLOCK_HOOKS}) is its plain hook when `owner` is a concern and nil otherwise; any
626
+ # other hook is itself.
627
+ def self.effective_hook(discovery, owner, hook)
628
+ plain = GATED_HOOKS[hook]
629
+ return hook if plain.nil?
630
+
631
+ concern_bucket?(discovery, owner) ? plain : nil
632
+ end
633
+
634
+ HOOK_KEY_SLOTS = {
635
+ DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND => 2, DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND => 1,
636
+ DiscoveryIndex::ENVELOPE_HOOK_DEF_KIND => 1
637
+ }.freeze
638
+ private_constant :HOOK_KEY_SLOTS
639
+
640
+ # The hook a hook-extend, open or def envelope key names (`:const_get` for a concern's), or nil for another key.
641
+ def self.hook_of(key)
642
+ slot = key.is_a?(Array) ? HOOK_KEY_SLOTS[key.first] : nil
643
+ slot && key[slot]
644
+ end
645
+
646
+ # The `[ENVELOPE_HOOK_EXTEND_KIND, written, hook]` keys of a module's envelope bucket, in recording order.
647
+ def self.hook_extend_keys(discovery, name)
648
+ bucket = discovery.discovered_parameter_envelopes[name]
649
+ return EMPTY_NAMES if bucket.nil?
650
+
651
+ bucket.each_key.select { |key| key.is_a?(Array) && key.first == DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND }
652
+ end
653
+
353
654
  # The hooks that let a module `extend`ed onto N reshape N's INSTANCE side (#1730): its singleton `extended` /
354
655
  # `extend_object` run against N at the `extend` and may mix into N; its instance `included` /
355
656
  # `append_features` / `prepended` / `prepend_features` / `inherited` become N's own singleton hooks and run
@@ -357,11 +658,44 @@ module Rigor
357
658
  SINGLETON_EXTEND_HOOKS = %i[extended extend_object].freeze
358
659
  INSTANCE_EXTEND_HOOKS = %i[included append_features prepended prepend_features inherited].freeze
359
660
 
360
- @extend_hook_keys = {}
661
+ # ADR-129 WD1 — three interning Memos, not run-scoped: a value is a frozen Array of frozen Strings built from the
662
+ # name and frozen constants alone, so an entry is fixed once written and holds nothing of a run. Each hit goes
663
+ # through `MemoAudit.check`, and an audit recomputation bypasses the table.
664
+ EXTEND_HOOK_KEYS = TypingState.memo(
665
+ :resolution_chain_extend_hook_keys,
666
+ audit: "extend hook keys",
667
+ reason: "keyed on the module name; a frozen Array of frozen Strings built from the name and the extend-hook " \
668
+ "constants alone"
669
+ )
670
+ HOOK_KEYS = TypingState.memo(
671
+ :resolution_chain_hook_keys,
672
+ audit: "hook keys",
673
+ reason: "keyed on the class name; a frozen Array of frozen Strings built from the name and " \
674
+ "Relevance::HOOKS alone"
675
+ )
676
+ EXTERNAL_KEYS = TypingState.memo(
677
+ :resolution_chain_external_keys,
678
+ audit: "external keys",
679
+ reason: "keyed on the class name; HOOK_KEYS' answer plus one frozen String built from the name"
680
+ )
681
+ private_constant :EXTEND_HOOK_KEYS, :HOOK_KEYS, :EXTERNAL_KEYS
361
682
 
362
683
  # The frozen negative method keys of `name`'s {SINGLETON_EXTEND_HOOKS} and {INSTANCE_EXTEND_HOOKS}.
363
684
  def self.extend_hook_keys(name)
364
- @extend_hook_keys[name] ||= (
685
+ return build_extend_hook_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
686
+
687
+ state = TypingState.current
688
+ table = (state[EXTEND_HOOK_KEYS] ||= {})
689
+ stored = table[name]
690
+ if stored.nil?
691
+ return table[name] = Inference::MemoAudit.key_audit("extend hook keys") { build_extend_hook_keys(name) }
692
+ end
693
+
694
+ Inference::MemoAudit.check("extend hook keys", stored, name) { build_extend_hook_keys(name) }
695
+ end
696
+
697
+ def self.build_extend_hook_keys(name)
698
+ (
365
699
  SINGLETON_EXTEND_HOOKS.map { |hook| "method:#{name}.#{hook}".freeze } +
366
700
  INSTANCE_EXTEND_HOOKS.map { |hook| "method:#{name}##{hook}".freeze }
367
701
  ).freeze
@@ -385,7 +719,7 @@ module Rigor
385
719
  end
386
720
 
387
721
  # `[[spelling, resolved name or nil, candidate names], …]`: every module the project entries of an instance chain
388
- # `extend`, as {Builder#extend_hook_taint?} resolved them.
722
+ # `extend`, as {Builder#extend_hook_taint} resolved them.
389
723
  def self.extend_targets(scope, flavor, entries)
390
724
  resolver = resolver_for(scope, flavor)
391
725
  extends = scope.discovery.discovered_extends
@@ -399,19 +733,37 @@ module Rigor
399
733
  end.uniq.freeze
400
734
  end
401
735
 
402
- @hook_keys = {}
403
- @external_keys = {}
404
-
405
736
  # The frozen `method:Name.hook` negative keys of `name`'s {Relevance::HOOKS}, built once per name.
406
737
  def self.hook_keys(name)
407
- @hook_keys[name] ||= Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
738
+ return build_hook_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
739
+
740
+ state = TypingState.current
741
+ table = (state[HOOK_KEYS] ||= {})
742
+ stored = table[name]
743
+ return table[name] = Inference::MemoAudit.key_audit("hook keys") { build_hook_keys(name) } if stored.nil?
744
+
745
+ Inference::MemoAudit.check("hook keys", stored, name) { build_hook_keys(name) }
408
746
  end
409
747
 
748
+ def self.build_hook_keys(name) = Relevance::HOOKS.map { |hook| "method:#{name}.#{hook}".freeze }.freeze
749
+
410
750
  # {hook_keys} plus the `mixin:<name>` key an external entry files for its unpositioned mixins (#1661).
411
751
  def self.external_keys(name)
412
- @external_keys[name] ||= (hook_keys(name) + ["mixin:#{name}".freeze]).freeze
752
+ return build_external_keys(name) if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
753
+
754
+ state = TypingState.current
755
+ table = (state[EXTERNAL_KEYS] ||= {})
756
+ stored = table[name]
757
+ if stored.nil?
758
+ return table[name] = Inference::MemoAudit.key_audit("external keys") { build_external_keys(name) }
759
+ end
760
+
761
+ Inference::MemoAudit.check("external keys", stored, name) { build_external_keys(name) }
413
762
  end
414
763
 
764
+ def self.build_external_keys(name) = (hook_keys(name) + ["mixin:#{name}".freeze]).freeze
765
+ private_class_method :build_extend_hook_keys, :build_hook_keys, :build_external_keys
766
+
415
767
  def record_head(scope, start, side)
416
768
  ResolutionChain.record_class(scope, @root) unless start.zero? && @entries.first&.name == @root
417
769
  start.times { |index| ResolutionChain.record_entry(scope, @entries[index], side) }
@@ -443,6 +795,25 @@ module Rigor
443
795
  @entries.index { |entry| entry.name == name && entry.side == side }
444
796
  end
445
797
 
798
+ # Issue #2979 — the position of the first entry after `name`'s own that is a true ancestor of it, or nil when
799
+ # `name` has no entry on `side`. The entries of the names bound to the same class object
800
+ # (`K = Struct.new("Named", …)`) follow `name`'s own directly, and a `def` under one of them is a redefinition
801
+ # of the class's method, not an override, so a reader of "the entries after the class" starts past them.
802
+ def ancestors_start(scope, name, side = @side)
803
+ own = index_of(name, side)
804
+ return nil if own.nil?
805
+
806
+ twins = DiscoveryIndex.same_class_names(scope.discovery.discovered_parameter_envelopes[name])
807
+ start = own + 1
808
+ start += 1 while twins_entry?(@entries[start], twins, side)
809
+ start
810
+ end
811
+
812
+ def twins_entry?(entry, twins, side)
813
+ !twins.empty? && !entry.nil? && entry.side == side && twins.include?(entry.name)
814
+ end
815
+ private :twins_entry?
816
+
446
817
  # A LEVEL is one superclass step: the class (or its singleton) with the modules inserted around it,
447
818
  # before the next superclass's entries begin. `level_classes[k]` is that class, or nil for a level that
448
819
  # is an external superclass. Only COMPLETE levels are counted — a level the {LIMIT} cut in half is not.
@@ -494,8 +865,21 @@ module Rigor
494
865
  end
495
866
  private :compute_levels
496
867
 
497
- MEMO_KEY = :__rigor_resolution_chain__
498
- private_constant :MEMO_KEY
868
+ # ADR-129 WD1 — a run-scoped Memo: one `[discovery, flavors]` slot keyed on the discovery index's identity, the
869
+ # only input a chain reads.
870
+ #
871
+ # ADR-129 G2 — every table of the slot's flavor buckets is audited on a hit under `RIGOR_MEMO_AUDIT=1`, and an
872
+ # audit recomputation reads a fresh, unstored bucket ({.flavor_bucket}), so it reads nothing stored.
873
+ MEMO = TypingState.memo(
874
+ :resolution_chain,
875
+ run_scoped: true,
876
+ audit: ["resolution chain", "resolution chain name", "resolution chain relevance",
877
+ "resolution chain master order", "resolution chain linearisation", "resolution chain mark",
878
+ "resolution chain entry", "resolution chain external entry", "singleton hook verdict"],
879
+ reason: "one slot keyed on the discovery index's identity; a chain is a pure function of that frozen index " \
880
+ "and the flavor"
881
+ )
882
+ private_constant :MEMO
499
883
 
500
884
  FLAVORS = %i[methods constants arity].freeze
501
885
 
@@ -507,17 +891,40 @@ module Rigor
507
891
 
508
892
  bucket = flavor_bucket(scope.discovery, flavor)
509
893
  chains = bucket[side]
510
- chains[class_name] || (chains[class_name] = Builder.new(scope, flavor, bucket).chain(class_name, side))
894
+ cached = chains[class_name]
895
+ if cached.nil?
896
+ return chains[class_name] = Inference::MemoAudit.key_audit("resolution chain") do
897
+ Builder.new(scope, flavor, bucket).chain(class_name, side)
898
+ end
899
+ end
900
+ return cached unless Inference::MemoAudit.enabled?
901
+
902
+ Inference::MemoAudit.check("resolution chain", cached.audit_signature, class_name, side) do
903
+ fresh = Analysis::DependencyRecorder.withhold do
904
+ Builder.new(scope, flavor, flavor_bucket(scope.discovery, flavor)).chain(class_name, side)
905
+ end
906
+ fresh.first.audit_signature
907
+ end
908
+ cached
511
909
  end
512
910
 
911
+ # The flavor's bucket of memo tables. While an audit recomputation runs ({Inference::MemoAudit.suspended?})
912
+ # every call answers a fresh bucket that is never stored, so the recomputation neither reads nor writes the
913
+ # slot.
513
914
  def self.flavor_bucket(discovery, flavor)
514
- slot = Thread.current[MEMO_KEY]
915
+ return new_bucket if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
916
+
917
+ state = TypingState.current
918
+ slot = state[MEMO]
515
919
  unless slot && slot[0].equal?(discovery)
516
920
  slot = [discovery, {}]
517
- Thread.current[MEMO_KEY] = slot
921
+ state[MEMO] = slot
518
922
  end
519
- slot[1][flavor] ||= { instance: {}, singleton: {}, names: {}, interned: {}, externals: {}, master: {},
520
- marks: {} }
923
+ slot[1][flavor] ||= new_bucket
924
+ end
925
+
926
+ def self.new_bucket
927
+ { instance: {}, singleton: {}, names: {}, interned: {}, externals: {}, master: {}, marks: {} }
521
928
  end
522
929
 
523
930
  # Which project class an as-written ancestor name denotes, under one flavor — shared by the chain and
@@ -534,9 +941,13 @@ module Rigor
534
941
  # ambiguous spelling names (`:arity` only), or nil.
535
942
  def resolve(owner, raw)
536
943
  by_owner = (@names[owner] ||= {})
537
- return by_owner[raw] if by_owner.key?(raw)
944
+ unless by_owner.key?(raw)
945
+ return by_owner[raw] = Inference::MemoAudit.key_audit("resolution chain name") { compute(owner, raw) }
946
+ end
538
947
 
539
- by_owner[raw] = compute(owner, raw)
948
+ Inference::MemoAudit.check("resolution chain name", by_owner[raw], owner, raw) do
949
+ Analysis::DependencyRecorder.withhold { compute(owner, raw) }.first
950
+ end
540
951
  end
541
952
 
542
953
  # {#resolve}, with an ambiguous spelling read as unresolved.
@@ -545,8 +956,28 @@ module Rigor
545
956
  resolved.is_a?(String) ? resolved : nil
546
957
  end
547
958
 
959
+ # Issue #1592 — {#resolve_one}, or else the first candidate the project declares at all: a module holding only
960
+ # hooks (`included do extend M end`) is no project class under any flavor, yet its hooks run.
961
+ # The search stops at the first candidate that exists in any form: one RBS or a gem declares and the project's
962
+ # source does not has no hooks the tables know, so it answers nil.
963
+ #
964
+ # Issue #3015 — a module whose bucket holds the concern entry counts too: its body runs `extend
965
+ # ActiveSupport::Concern`, so the project declares it. A one-file index has no `discovered_class_sources`, and
966
+ # would otherwise drop a concern whose only defs sit in its blocks from an includer's chain.
967
+ def resolve_declared(owner, raw)
968
+ candidates(owner, raw).each do |candidate|
969
+ return candidate if project?(candidate) || @discovery.discovered_class_sources.key?(candidate) ||
970
+ @discovery.discovered_parameter_envelopes[candidate]&.key?(CONCERN_KEY)
971
+ return nil if rbs_known?(candidate)
972
+ end
973
+ nil
974
+ end
975
+
548
976
  def candidates(owner, raw) = @scope.ancestor_name_candidates(owner, raw)
549
977
 
978
+ # Whether `name`, already qualified, is a project class under this flavor.
979
+ def project_name?(name) = project?(name)
980
+
550
981
  private
551
982
 
552
983
  def compute(owner, raw)
@@ -557,6 +988,12 @@ module Rigor
557
988
  alternatives.empty? ? nil : alternatives
558
989
  end
559
990
 
991
+ def rbs_known?(name)
992
+ Rigor::Reflection.rbs_class_known?(name, scope: @scope)
993
+ rescue StandardError
994
+ false
995
+ end
996
+
560
997
  def project?(name)
561
998
  case @flavor
562
999
  when :constants
@@ -572,7 +1009,7 @@ module Rigor
572
1009
  end
573
1010
  private_constant :Resolver
574
1011
 
575
- private_class_method :flavor_bucket
1012
+ private_class_method :flavor_bucket, :new_bucket
576
1013
 
577
1014
  # {Relevance}'s verdict memo, one table per flavor bucket.
578
1015
  def self.relevance_memo(scope, flavor)
@@ -716,9 +1153,16 @@ module Rigor
716
1153
  def self.master_memo(scope, flavor, key)
717
1154
  bucket = flavor_bucket(scope.discovery, flavor)
718
1155
  memo = bucket[:master]
719
- return memo[key] if memo.key?(key)
1156
+ unless memo.key?(key)
1157
+ return memo[key] = Inference::MemoAudit.key_audit("resolution chain master order") do
1158
+ yield(Resolver.new(scope, flavor, bucket))
1159
+ end
1160
+ end
720
1161
 
721
- memo[key] = yield(Resolver.new(scope, flavor, bucket))
1162
+ Inference::MemoAudit.check("resolution chain master order", memo[key], flavor, key) do
1163
+ fresh = flavor_bucket(scope.discovery, flavor)
1164
+ Analysis::DependencyRecorder.withhold { yield(Resolver.new(scope, flavor, fresh)) }.first
1165
+ end
722
1166
  end
723
1167
 
724
1168
  # The linearisation itself, in one world: Ruby's skip rule (`retro: false`), or every skipped insertion
@@ -768,7 +1212,7 @@ module Rigor
768
1212
  # The chain for `root`, carrying its retro world when the skip rule skipped anything on the way.
769
1213
  def chain(root, side)
770
1214
  entries, starts, classes, forks, marks, retro_ok = lin_for(root, side)
771
- order_marked = marks.any? { |mark| mark.kind != :extend_hook }
1215
+ order_marked = marks.any? { |mark| !HOOK_MARK_KINDS.include?(mark.kind) }
772
1216
  retro = build_retro(root, side) if forks == 1 && retro_ok && !order_marked && !@retro
773
1217
  cut_at = cut_position(entries)
774
1218
  return whole_chain(root, side, entries, starts, classes, [forks, marks], retro) if cut_at.nil?
@@ -827,14 +1271,10 @@ module Rigor
827
1271
  memoised(@retro ? :lin_singleton_retro : :lin_singleton, name, depth) { compute_singleton(name, depth) }
828
1272
  end
829
1273
 
830
- def memoised(table, name, depth)
1274
+ def memoised(table, name, depth, &)
831
1275
  memo = (@bucket[table] ||= {})
832
1276
  cached = memo[name]
833
- if cached
834
- # A memoised chain's forks are the asker's too.
835
- @frames.last&.absorb(cached[3], cached[4], cached[5])
836
- return cached
837
- end
1277
+ return absorb_cached(audited_lin(cached, table, name)) if cached
838
1278
 
839
1279
  # A cycle (`class Foo < Foo` resolved inside its own namespace) or a hierarchy deeper than the budget
840
1280
  # contributes nothing past that point, and the chains computed while it did are not memoised: they
@@ -849,7 +1289,7 @@ module Rigor
849
1289
  frame = Frame.new(0, false, NO_MARKS, true)
850
1290
  @frames.push(frame)
851
1291
  stack.push(name)
852
- entries, starts, classes = yield
1292
+ entries, starts, classes = Inference::MemoAudit.key_audit("resolution chain linearisation", &)
853
1293
  stack.pop
854
1294
  @frames.pop
855
1295
  lin = [entries, starts, classes, frame.forks, frame.marks, frame.retro_ok].freeze
@@ -858,6 +1298,29 @@ module Rigor
858
1298
  lin
859
1299
  end
860
1300
 
1301
+ # A memoised linearisation rebuilt for the audit by a Builder over a fresh bucket. A memoised one met no
1302
+ # cycle and stayed inside the depth budget, so its depth does not change it; a retro world that fit
1303
+ # {LIMIT} once fits it again.
1304
+ # A memoised chain's forks are the asker's too.
1305
+ def absorb_cached(cached)
1306
+ @frames.last&.absorb(cached[3], cached[4], cached[5])
1307
+ cached
1308
+ end
1309
+
1310
+ def audited_lin(cached, table, name)
1311
+ Inference::MemoAudit.check("resolution chain linearisation", cached, table, name) { fresh_lin(table, name) }
1312
+ end
1313
+
1314
+ SINGLETON_LIN_TABLES = %i[lin_singleton lin_singleton_retro].freeze
1315
+ private_constant :SINGLETON_LIN_TABLES
1316
+
1317
+ def fresh_lin(table, name)
1318
+ side = SINGLETON_LIN_TABLES.include?(table) ? :singleton : :instance
1319
+ builder = Builder.new(@scope, @flavor, ResolutionChain.send(:flavor_bucket, @discovery, @flavor),
1320
+ retro: @retro)
1321
+ Analysis::DependencyRecorder.withhold { builder.send(:lin_for, name, side) }.first
1322
+ end
1323
+
861
1324
  def hand_up(frame)
862
1325
  parent = @frames.last
863
1326
  return if parent.nil?
@@ -867,9 +1330,11 @@ module Rigor
867
1330
  end
868
1331
 
869
1332
  def compute_instance(name, depth)
870
- # `discovered_includes` already carries the prepended names, so it counts every instance-side edge once.
871
- mark_unsettled(name, :include, (@discovery.discovered_includes[name] || EMPTY).size)
872
- @frames.last.add_marks(mark_for(name, :extend_hook, WILDCARD_LIST, false)) if extend_hook_taint?(name)
1333
+ twins = same_class_twins(name)
1334
+ mark_instance_node(name, :include)
1335
+ twins.each { |owner| mark_instance_node(owner, :include) }
1336
+ return same_class_instance(name, twins, depth) unless twins.empty?
1337
+
873
1338
  entries = [project_entry(name, :instance)]
874
1339
  super_lin = superclass_lin(name, :instance, depth)
875
1340
  entries.concat(super_lin[0])
@@ -881,12 +1346,333 @@ module Rigor
881
1346
  end
882
1347
 
883
1348
  def compute_singleton(name, depth)
884
- mark_unsettled(name, :extend, (@discovery.discovered_extends[name] || EMPTY).size)
1349
+ twins = same_class_twins(name)
1350
+ own = @discovery.discovered_extends[name] || EMPTY
1351
+ own_prepends = own_singleton_prepends(name, own)
1352
+ mark_unsettled(name, :extend, singleton_order_edges(own, own_prepends))
1353
+ twins.each { |owner| mark_unsettled(owner, :extend, (@discovery.discovered_extends[owner] || EMPTY).size) }
1354
+ return same_class_singleton(name, twins, depth) unless twins.empty?
1355
+
885
1356
  entries = [project_entry(name, :singleton)]
886
1357
  super_lin = superclass_lin(name, :singleton, depth)
887
1358
  entries.concat(super_lin[0])
888
- extends = @discovery.discovered_extends[name] || EMPTY
889
- super_start = include_all(entries, name, extends, EMPTY, depth, [0, 1])
1359
+ own_extends = own_prepends.empty? ? own : own - own_prepends
1360
+ extends, prepends = singleton_mixins(name, own_extends, own_prepends)
1361
+ boundary = prepends.empty? ? CLASS_FIRST : prepend_all(entries, name, prepends, depth)
1362
+ super_start = include_all(entries, name, extends, EMPTY, depth, boundary)
1363
+ finish(entries, name, super_lin, super_start)
1364
+ end
1365
+
1366
+ # #1592 — the class's own `extend`s and the modules its direct mixins' hooks extend it with
1367
+ # ({#hook_extensions}), nearest first. Ruby inserts each where its statement ran, and the tables keep the
1368
+ # `extend` and `include` statements apart, so with both kinds present the two blocks' order is a fork: the
1369
+ # chain puts the own `extend`s nearer (the usual layout writes them after the `include`s) and the retro world
1370
+ # the hooks' modules, and a reader that the two worlds answer alike stands, since then only one block can
1371
+ # answer the name. An order the hooks do not fix, or an include whose own position is not a fact, is two
1372
+ # forks, which settle to master. Answers `[extends, prepends]`: a concern's `ClassMethods` on a prepend edge is
1373
+ # prepended to the class object (`Concern#prepend_features` runs `base.singleton_class.prepend`), so it sits
1374
+ # ahead of the class object's own entry.
1375
+ #
1376
+ # Issue #2998 — `own_prepends` are the class's own singleton-side prepends ({#own_singleton_prepends}), which
1377
+ # sit ahead of the class object too. An `extend` always sits behind the class object and a prepend always
1378
+ # ahead of it, so only two prepends can trade places ({#order_prepends}).
1379
+ def singleton_mixins(name, extends, own_prepends)
1380
+ plan = hook_extensions(name)
1381
+ if plan.open || unnamed_instance_mixin?(name)
1382
+ @frames.last.add_marks(mark_for(name, :include_hook, UNNAMED_LIST, false))
1383
+ end
1384
+ fork_hook_order(name, 2) unless plan.ordered || (plan.extends.empty? && plan.prepends.empty?)
1385
+ prepends = order_prepends(name, own_prepends, plan.prepends)
1386
+ hooked = plan.extends
1387
+ return [extends, prepends] if hooked.empty?
1388
+ return [hooked, prepends] if extends.empty?
1389
+
1390
+ fork_hook_order(name, 1)
1391
+ [@retro ? hooked + extends : extends + hooked, prepends]
1392
+ end
1393
+
1394
+ # The class object's prepends, nearest first, once its own are placed against its hooks'. Where their order
1395
+ # is not a fact (own and hook prepends both, or two own ones from two files or with an edge whose position is
1396
+ # not a fact), the chain counts two forks, which settle to master, and the class takes a `:prepend_order`
1397
+ # mark: the typing read declines a definer from its prepend region, since neither order is Ruby's for sure.
1398
+ def order_prepends(name, own, hooked)
1399
+ return hooked if own.empty?
1400
+
1401
+ if !hooked.empty? || (own.size > 1 && !own_prepends_ordered?(name))
1402
+ @frames.last.forks += 2
1403
+ @frames.last.add_marks(mark_for(name, :prepend_order, nil, false))
1404
+ end
1405
+ own + hooked
1406
+ end
1407
+
1408
+ def own_prepends_ordered?(name)
1409
+ !multi_file?(name) && (@discovery.unpositioned_mixins[name]&.dig(:extend) || EMPTY).empty?
1410
+ end
1411
+
1412
+ # Issue #2998 — the spellings of `name`'s singleton edges (`discovered_extends`) that are prepends: a
1413
+ # `class << self; prepend P` or a `singleton_class.prepend P` the walk filed as a
1414
+ # `[ENVELOPE_SINGLETON_PREPEND_KIND, "P"]` entry. They keep the table's order, nearest first, which is the
1415
+ # prepends' own: a later `prepend` sits ahead of an earlier one. A class with any takes the `:singleton_prepend`
1416
+ # mark, which tells the typing read that master's order, which knows no singleton prepend, may answer
1417
+ # otherwise.
1418
+ #
1419
+ # Issue #3020 — a prepend whose module no table names (`class << self; prepend(*MODS)`,
1420
+ # `singleton_class.prepend(*MODS)`) files the `"?"` entry instead, which no edge spells: the class takes the
1421
+ # `:unnamed_prepend` mark, and the typing read declines a definer at or behind the class object.
1422
+ def own_singleton_prepends(name, own)
1423
+ bucket = @discovery.discovered_parameter_envelopes[name]
1424
+ return EMPTY if bucket.nil?
1425
+
1426
+ @frames.last.add_marks(mark_for(name, :unnamed_prepend, nil, false)) if bucket.key?(UNNAMED_PREPEND_KEY)
1427
+ return EMPTY if own.empty?
1428
+
1429
+ prepends = own.select { |raw| bucket.key?([DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, raw]) }
1430
+ return EMPTY if prepends.empty?
1431
+
1432
+ @frames.last.add_marks(mark_for(name, :singleton_prepend, nil, false))
1433
+ prepends
1434
+ end
1435
+
1436
+ # The edge count the multi-file rule weighs: a prepend sits ahead of the class object and an `extend` behind
1437
+ # it whatever the load order, so only edges of one kind can trade places across files.
1438
+ def singleton_order_edges(own, own_prepends)
1439
+ return own.size if own_prepends.empty?
1440
+
1441
+ [own_prepends.size, own.size - own_prepends.size].max
1442
+ end
1443
+
1444
+ def fork_hook_order(name, weight)
1445
+ @frames.last.forks += weight
1446
+ @frames.last.add_marks(mark_for(name, :hook_order, nil, false))
1447
+ end
1448
+
1449
+ # The {HookPlan} for a class: the `[owner, spelling]` pairs of the modules the hooks of its direct
1450
+ # mixins extend it with, nearest first (a later include's ahead of an earlier one's, and within one mixin the
1451
+ # hook's own extends ahead of a concern's `ClassMethods`, which `append_features` extends before the
1452
+ # `included` block runs); the concern `ClassMethods` its prepend edges prepend to its singleton; whether one of
1453
+ # those hooks may extend it with a module no entry names; and whether the order is a fact. A concern includer
1454
+ # gets nothing from a concern: `Concern#append_features` records it as a dependency instead (#2992).
1455
+ def hook_extensions(name)
1456
+ includes = @discovery.discovered_includes[name] || EMPTY
1457
+ return NO_HOOKS if includes.empty?
1458
+
1459
+ listed = @discovery.unpositioned_mixins[name]&.dig(:include) || EMPTY
1460
+ plan = HookPlan.new([], [], false, true)
1461
+ each_hooked_mixin(name, includes) do |raw, mixin, hooks, loose|
1462
+ unpositioned = loose || listed.include?(raw) || listed.include?(WILDCARD_LIST.first)
1463
+ merge_targets(plan, hook_targets(mixin, hooks), unpositioned)
1464
+ plan.open ||= uncertain_dependencies?(mixin)
1465
+ end
1466
+ # Two files' includes run in load order.
1467
+ plan.ordered &&= plan.extends.size + plan.prepends.size < 2 || !multi_file?(name)
1468
+ plan
1469
+ end
1470
+
1471
+ # Adds one mixin's targets to the class's plan; an include whose own position is not a fact (`unpositioned`)
1472
+ # leaves its modules' order unknown.
1473
+ def merge_targets(plan, targets, unpositioned)
1474
+ plan.open ||= targets.open
1475
+ return if targets.extends.empty? && targets.prepends.empty?
1476
+
1477
+ plan.ordered &&= targets.ordered && !unpositioned
1478
+ plan.extends.concat(targets.extends)
1479
+ plan.prepends.concat(targets.prepends)
1480
+ end
1481
+
1482
+ # Issue #2994 — whether a module no table names may be mixed into `name`'s instance side by a call whose target
1483
+ # is `name` itself, so that its `included` / `prepended` hook runs with `name` as `base` and may extend the
1484
+ # class object with anything: `name` lists {Relevance::UNNAMED} on its own instance side (`include(*MODS)`,
1485
+ # `include helper`), or a module it includes does (a hook's `base.include(*MODS)`; that module's own
1486
+ # `include(*MODS)` is read so too). A superclass's own mark is read when its singleton chain is built. The
1487
+ # entries this reads are the class's own sites and its direct mixins', which a singleton chain's readers file
1488
+ # already. An extended module's hook that splats a module in already declines the read on another route.
1489
+ def unnamed_instance_mixin?(name)
1490
+ return true if lists_unnamed?(name)
1491
+
1492
+ (@discovery.discovered_includes[name] || EMPTY).uniq.any? do |raw|
1493
+ mixin = @resolver.resolve_declared(name, raw)
1494
+ !mixin.nil? && lists_unnamed?(mixin)
1495
+ end
1496
+ end
1497
+
1498
+ def lists_unnamed?(name)
1499
+ listed = @discovery.unpositioned_mixins[name]&.dig(:include)
1500
+ !listed.nil? && listed.include?(UNNAMED_LIST.last)
1501
+ end
1502
+
1503
+ HookPlan = Struct.new(:extends, :prepends, :open, :ordered)
1504
+ private_constant :HookPlan
1505
+
1506
+ # Yields each direct mixin of `name`, nearest first, as `[spelling, resolved, hooks]`: the hooks its edges
1507
+ # run, `:included` for an `include` and `:prepended` for a `prepend`.
1508
+ #
1509
+ # Issue #2992 — a concern mixin's dependencies follow it, each on the same edge
1510
+ # ({ResolutionChain.concern_dependencies}): `Concern#append_features` includes them into `name` ahead of the
1511
+ # concern itself, so their modules sit farther than the concern's. `spelling` is the mixin's for each, and
1512
+ # `loose` says a dependency's place is not a fact: `Concern` puts a prepended dependency ahead of the included
1513
+ # ones, which the tables list the other way, and two files of the concern add theirs in load order.
1514
+ def each_hooked_mixin(name, includes)
1515
+ prepends = @discovery.discovered_prepends[name] || EMPTY
1516
+ concern = concern?(name)
1517
+ includes.uniq.each do |raw|
1518
+ mixin = @resolver.resolve_declared(name, raw)
1519
+ next if mixin.nil? || (concern && concern?(mixin))
1520
+
1521
+ hooks = []
1522
+ hooks << :prepended if prepends.include?(raw)
1523
+ hooks << :included if includes.count(raw) > prepends.count(raw)
1524
+ yield raw, mixin, hooks, false
1525
+ dependencies = ResolutionChain.concern_dependencies(@discovery, @resolver, mixin)
1526
+ loose = dependencies.size > 1 && dependencies.any? { |member| loose_dependencies?(member, mixin) }
1527
+ dependencies.each { |dependency| yield raw, dependency, hooks, loose }
1528
+ end
1529
+ end
1530
+
1531
+ # Issue #2992 — whether a concern declared in two files includes a concern its dependency entries do not name:
1532
+ # written in a reopening without the `extend ActiveSupport::Concern`, that `include` is a dependency when the
1533
+ # reopening loads after the `extend` and an ordinary include of the concern itself when it loads first, so the
1534
+ # includer may or may not be extended with that concern's modules.
1535
+ def uncertain_dependencies?(mixin)
1536
+ return false unless concern?(mixin) && multi_file?(mixin)
1537
+
1538
+ spellings = ResolutionChain.dependency_spellings(@discovery, mixin)
1539
+ (@discovery.discovered_includes[mixin] || EMPTY).any? do |raw|
1540
+ next false if spellings.include?(raw)
1541
+
1542
+ dependency = @resolver.resolve_declared(mixin, raw)
1543
+ !dependency.nil? && concern?(dependency)
1544
+ end
1545
+ end
1546
+
1547
+ # Whether the concern `owner` or its dependency `member` prepends a module or is declared in two files.
1548
+ def loose_dependencies?(member, owner)
1549
+ [member, owner].any? { |name| multi_file?(name) || !(@discovery.discovered_prepends[name] || EMPTY).empty? }
1550
+ end
1551
+
1552
+ NO_HOOKS = HookPlan.new(EMPTY, EMPTY, false, true).freeze
1553
+ private_constant :NO_HOOKS
1554
+
1555
+ def concern?(name)
1556
+ bucket = @discovery.discovered_parameter_envelopes[name]
1557
+ !bucket.nil? && bucket.key?(CONCERN_KEY)
1558
+ end
1559
+
1560
+ # One mixin's hook-extended modules for the edges `hooks` runs, as {#hook_extensions} answers them.
1561
+ # A hook defined twice (`ENVELOPE_HOOK_DEF_KIND`) is open: which definition Ruby keeps is load order.
1562
+ #
1563
+ # Issue #3005 — a hook block's `def self.x`s (`<included>`) sit on the class object itself, ahead of every
1564
+ # module the block extends it with, so that module goes first and does not make the order unknown.
1565
+ def hook_targets(mixin, hooks)
1566
+ named, open = hook_keys(mixin, hooks)
1567
+ blocks, modules = named.partition { |raw| DiscoveryIndex::HOOK_BLOCK_SEGMENTS.include?(raw) }
1568
+ pairs = (blocks + modules).map { |raw| hook_target_pair(mixin, raw) }
1569
+ targets = HookPlan.new(pairs, [], open, modules.size <= 1)
1570
+ found = concern?(mixin) && const_get_target(mixin, "ClassMethods")
1571
+ (hooks.include?(:prepended) ? targets.prepends : targets.extends) << [mixin, "::#{found}"] if found
1572
+ targets
1573
+ end
1574
+
1575
+ # `[named, open]`: the spellings the mixin's hooks for `hooks` extend with, and whether one of them is open
1576
+ # or defined twice.
1577
+ def hook_keys(mixin, hooks)
1578
+ named = []
1579
+ open = false
1580
+ defs = Hash.new(0)
1581
+ (@discovery.discovered_parameter_envelopes[mixin] || EMPTY_BUCKET).each_key do |key|
1582
+ next unless hooks.include?(ResolutionChain.effective_hook(@discovery, mixin, ResolutionChain.hook_of(key)))
1583
+
1584
+ case key.first
1585
+ when DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND then named << key[1]
1586
+ when DiscoveryIndex::ENVELOPE_HOOK_EXTEND_OPEN_KIND then open = true
1587
+ else defs[key[1]] += 1
1588
+ end
1589
+ end
1590
+ [named.uniq, open || defs.each_value.any? { |count| count > 1 }]
1591
+ end
1592
+
1593
+ EMPTY_BUCKET = {}.freeze
1594
+ private_constant :EMPTY_BUCKET
1595
+
1596
+ # The module a hook's constant names, written in the mixin's body: `Mixin::Raw` first (the mixin is the
1597
+ # innermost cref there), then the names its header's nesting gives. A name no project module answers stays
1598
+ # the mixin's spelling, an external entry.
1599
+ def hook_target_pair(mixin, raw)
1600
+ own = "#{mixin}::#{raw}"
1601
+ return [mixin, "::#{own}"] if !raw.start_with?("::") && @resolver.project_name?(own)
1602
+
1603
+ [mixin, raw]
1604
+ end
1605
+
1606
+ # `mixin.const_get(raw)`, as `Concern#append_features` looks `ClassMethods` up: `Name::Raw` for each entry of
1607
+ # the mixin's own instance chain, then a top-level `Raw`; nil when no project module answers, which Concern
1608
+ # then does not extend.
1609
+ #
1610
+ # Issue #2992 — a concern the mixin depends on is no ancestor of it ({ResolutionChain.concern_dependencies}),
1611
+ # so its `ClassMethods` is not found here: it reaches the includer as that dependency's own.
1612
+ def const_get_target(mixin, raw)
1613
+ chain = ResolutionChain.for(@scope, mixin, :instance, @flavor)
1614
+ dependencies = ResolutionChain.concern_dependencies(@discovery, @resolver, mixin)
1615
+ chain.entries.each do |entry|
1616
+ next if dependencies.include?(entry.name)
1617
+
1618
+ names = entry.external? ? entry.candidates : [entry.name]
1619
+ found = names.map { |owner| [owner, "#{owner}::#{raw}"] }.find do |owner, candidate|
1620
+ @resolver.project_name?(candidate) && ResolutionChain.const_get_candidate?(@discovery, owner, candidate)
1621
+ end&.last
1622
+ return found if found
1623
+ end
1624
+ @resolver.project_name?(raw) ? raw : nil
1625
+ end
1626
+
1627
+ # The instance-side marks of one node; `kind` is the side's mixin kind, `:include`. `discovered_includes`
1628
+ # already carries the prepended names, so it counts every instance-side edge once.
1629
+ def mark_instance_node(name, kind)
1630
+ mark_unsettled(name, kind, (@discovery.discovered_includes[name] || EMPTY).size)
1631
+ hook = extend_hook_taint(name)
1632
+ @frames.last.add_marks(mark_for(name, :extend_hook, hook, false)) if hook
1633
+ end
1634
+
1635
+ # Issue #2979 — the names `name`'s envelope bucket binds to the same class object (`K = Struct.new("Named",
1636
+ # …)` makes `K` and `Struct::Named` one class), without `name`; empty, and allocating nothing, for nearly every
1637
+ # class. The chain of either name heads with every name's entry, so a reader asks each name's own tables where
1638
+ # Ruby asks the one class's; nothing is copied between the names. Only the direct binding counts: two
1639
+ # `Struct.new("Named", …)` writes make two classes in Ruby, and neither constant reaches the other's tables
1640
+ # through `Struct::Named`. {ResolutionChain#ancestors_start} tells a reader where the true ancestors begin.
1641
+ def same_class_twins(name)
1642
+ twins = DiscoveryIndex.same_class_names(@discovery.discovered_parameter_envelopes[name])
1643
+ twins.include?(name) ? twins - [name] : twins
1644
+ end
1645
+
1646
+ # The instance side of a class with twins: every name's prepends ahead of the class, then every name's
1647
+ # includes after the LAST name, so the names stay ahead of every module either one includes, as the one class
1648
+ # heads its own ancestors. The superclass edge stays `name`'s own: a Struct's class has none the tables
1649
+ # record, under either name.
1650
+ def same_class_instance(name, twins, depth)
1651
+ names = [name, *twins].uniq
1652
+ entries = names.map { |owner| project_entry(owner, :instance) }
1653
+ super_lin = superclass_lin(name, :instance, depth)
1654
+ entries.concat(super_lin[0])
1655
+ boundary = [0, names.size]
1656
+ names.each do |owner|
1657
+ boundary = prepend_all(entries, owner, @discovery.discovered_prepends[owner] || EMPTY, depth, boundary)
1658
+ end
1659
+ super_start = names.reduce(boundary[1]) do |start, owner|
1660
+ include_all(entries, owner, @discovery.discovered_includes[owner] || EMPTY,
1661
+ @discovery.discovered_prepends[owner] || EMPTY, depth, [boundary[0] + names.size - 1, start])
1662
+ end
1663
+ finish(entries, name, super_lin, super_start)
1664
+ end
1665
+
1666
+ # The singleton twin of {#same_class_instance}: every name's class-object entry, then every name's extends.
1667
+ def same_class_singleton(name, twins, depth)
1668
+ names = [name, *twins].uniq
1669
+ entries = names.map { |owner| project_entry(owner, :singleton) }
1670
+ super_lin = superclass_lin(name, :singleton, depth)
1671
+ entries.concat(super_lin[0])
1672
+ super_start = names.reduce(names.size) do |start, owner|
1673
+ include_all(entries, owner, @discovery.discovered_extends[owner] || EMPTY, EMPTY, depth,
1674
+ [names.size - 1, start])
1675
+ end
890
1676
  finish(entries, name, super_lin, super_start)
891
1677
  end
892
1678
 
@@ -908,24 +1694,53 @@ module Rigor
908
1694
  # The one-element frozen array carrying the {Mark} of `[name, kind]`, interned in the bucket.
909
1695
  def mark_for(name, kind, listed, multi)
910
1696
  by_node = (@bucket[:marks][kind] ||= {})
911
- by_node[name] ||= [ResolutionChain::Mark.new(name, kind, listed.nil? ? EMPTY : listed.to_a.freeze,
912
- multi)].freeze
1697
+ cached = by_node[name]
1698
+ if cached.nil?
1699
+ return by_node[name] = Inference::MemoAudit.key_audit("resolution chain mark") do
1700
+ new_mark(name, kind, listed, multi)
1701
+ end
1702
+ end
1703
+ return cached unless Inference::MemoAudit.enabled?
1704
+
1705
+ # The failure names the whole key: the bucket's flavor, the mark's kind, the node.
1706
+ Inference::MemoAudit.check("resolution chain mark", cached, [@flavor, kind], name) do
1707
+ new_mark(name, kind, listed, multi)
1708
+ end
913
1709
  end
914
1710
 
915
- WILDCARD_LIST = ["*"].freeze # Relevance::WILDCARD, which loads after this file
916
- private_constant :WILDCARD_LIST
1711
+ def new_mark(name, kind, listed, multi)
1712
+ [ResolutionChain::Mark.new(name, kind, listed.nil? ? EMPTY : listed.to_a.freeze, multi)].freeze
1713
+ end
1714
+
1715
+ # The `[origin, super_start]` of a one-name chain: the class at 0, its superclass's entries from 1.
1716
+ CLASS_FIRST = [0, 1].freeze
1717
+ private_constant :CLASS_FIRST
917
1718
 
918
1719
  # #1730 — whether a module `name` extends has a hook that may mix into `name`'s instance side or its
919
1720
  # includers' ({ResolutionChain.extend_reshapes_instance?}). Ruby runs it at the `extend`, so the mixin is
920
1721
  # not a fact of the tables and `name`'s instance side is unpositioned. A spelling no project module
921
1722
  # answers taints nothing: the project cannot see its hooks.
922
- def extend_hook_taint?(name)
923
- (@discovery.discovered_extends[name] || EMPTY).any? do |raw|
1723
+ #
1724
+ # The mark's list: nil when no extended module taints, else {UNNAMED_LIST} when one of those modules lists `"?"`
1725
+ # (#2991), {WILDCARD_LIST} otherwise. Only the marker carries over: a name such a module lists is spelled
1726
+ # relative to that module, not to the extender.
1727
+ def extend_hook_taint(name)
1728
+ found = nil
1729
+ (@discovery.discovered_extends[name] || EMPTY).each do |raw|
924
1730
  resolved = @resolver.resolve_one(name, raw)
925
- !resolved.nil? && ResolutionChain.extend_reshapes_instance?(@scope, resolved)
1731
+ next if resolved.nil? || !ResolutionChain.extend_reshapes_instance?(@scope, resolved)
1732
+ return UNNAMED_LIST if @discovery.unpositioned_mixins[resolved][:include].include?(UNNAMED_LIST.last)
1733
+
1734
+ found = WILDCARD_LIST
926
1735
  end
1736
+ found
927
1737
  end
928
1738
 
1739
+ WILDCARD_LIST = ["*"].freeze # Relevance::WILDCARD, which loads after this file
1740
+ UNNAMED_LIST = ["*", "?"].freeze # with Relevance::UNNAMED
1741
+ UNNAMED_PREPEND_KEY = [DiscoveryIndex::ENVELOPE_SINGLETON_PREPEND_KIND, "?"].freeze # with Relevance::UNNAMED
1742
+ private_constant :WILDCARD_LIST, :UNNAMED_LIST, :UNNAMED_PREPEND_KEY
1743
+
929
1744
  def multi_file?(name)
930
1745
  sites = @discovery.discovered_class_sources[name]
931
1746
  !sites.nil? && sites.size >= 2
@@ -970,14 +1785,15 @@ module Rigor
970
1785
  # module's own includes may have run before or after), and the tables record `include M; prepend M`
971
1786
  # exactly as a plain `prepend M`. A direct prepend of the module itself is always inserted, in Ruby and
972
1787
  # here, so it forks nothing.
973
- def prepend_all(entries, owner, prepends, depth)
974
- origin = 0
975
- super_start = 1
1788
+ def prepend_all(entries, owner, prepends, depth, boundary = CLASS_FIRST)
1789
+ origin, super_start = boundary
976
1790
  # The table holds every statement, nearest first: a same-owner repeat is a no-op in Ruby (the first
977
1791
  # statement wins), so it is dropped here rather than met as a fork.
978
1792
  prepends.reverse.uniq.each do |raw|
979
- modules = Array(resolve(owner, raw))
980
- each_mixin_chain(owner, raw, depth) do |sub|
1793
+ # A hook's module comes as `[owner, spelling]`, as in {#include_all}.
1794
+ from, raw = raw.is_a?(Array) ? raw : [owner, raw]
1795
+ modules = Array(resolve(from, raw))
1796
+ each_mixin_chain(from, raw, depth) do |sub|
981
1797
  point = -1
982
1798
  sub.each do |entry|
983
1799
  found = entries.index(entry)
@@ -1008,7 +1824,9 @@ module Rigor
1008
1824
  mixins.reverse_each do |raw|
1009
1825
  next if skip.include?(raw)
1010
1826
 
1011
- each_mixin_chain(owner, raw, depth) do |sub|
1827
+ # A hook's module comes as `[owner, spelling]`: the mixin whose body wrote it resolves it.
1828
+ from, raw = raw.is_a?(Array) ? raw : [owner, raw]
1829
+ each_mixin_chain(from, raw, depth) do |sub|
1012
1830
  point = origin
1013
1831
  sub.each_with_index do |entry, index|
1014
1832
  found = @retro ? nil : entries.index(entry)
@@ -1049,14 +1867,43 @@ module Rigor
1049
1867
 
1050
1868
  def project_entry(name, side)
1051
1869
  by_side = (@bucket[:interned][side] ||= {})
1052
- by_side[name] ||= Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze, nil)
1870
+ cached = by_side[name]
1871
+ if cached.nil?
1872
+ return by_side[name] = Inference::MemoAudit.key_audit("resolution chain entry") do
1873
+ new_project_entry(name, side)
1874
+ end
1875
+ end
1876
+ return cached unless Inference::MemoAudit.enabled?
1877
+
1878
+ Inference::MemoAudit.check("resolution chain entry", cached, [@flavor, side], name) do
1879
+ new_project_entry(name, side)
1880
+ end
1881
+ end
1882
+
1883
+ def new_project_entry(name, side)
1884
+ Entry.new(name, side, nil, nil, false, name.to_s.split("::").last.freeze, nil)
1053
1885
  end
1054
1886
 
1055
1887
  def external_entry(owner, raw, side, superclass_edge)
1056
1888
  by_edge = ((@bucket[:externals][side] ||= {})[superclass_edge] ||= {})
1057
1889
  by_owner = (by_edge[owner] ||= {})
1058
- by_owner[raw] ||= Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze,
1059
- superclass_edge, nil, owner)
1890
+ cached = by_owner[raw]
1891
+ if cached.nil?
1892
+ return by_owner[raw] = Inference::MemoAudit.key_audit("resolution chain external entry") do
1893
+ new_external_entry(owner, raw, side, superclass_edge)
1894
+ end
1895
+ end
1896
+ return cached unless Inference::MemoAudit.enabled?
1897
+
1898
+ # The failure names the whole key: flavor, side, superclass edge, owner and spelling.
1899
+ Inference::MemoAudit.check("resolution chain external entry", cached, [@flavor, side, superclass_edge],
1900
+ [owner, raw]) do
1901
+ Analysis::DependencyRecorder.withhold { new_external_entry(owner, raw, side, superclass_edge) }.first
1902
+ end
1903
+ end
1904
+
1905
+ def new_external_entry(owner, raw, side, superclass_edge)
1906
+ Entry.new(nil, side, raw, @scope.ancestor_name_candidates(owner, raw).freeze, superclass_edge, nil, owner)
1060
1907
  end
1061
1908
  end
1062
1909
  private_constant :Builder