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
@@ -10,8 +10,12 @@ require_relative "member_consistency"
10
10
  require_relative "required_features"
11
11
  require_relative "gated_signature_guard"
12
12
  require_relative "gated_signature_plan"
13
+ require_relative "deferred_signature_plan"
14
+ require_relative "vendored_signature_plan"
13
15
  require_relative "ruby_library_roots"
16
+ require_relative "core_root"
14
17
  require_relative "../rooted_glob"
18
+ require_relative "../typing_state"
15
19
 
16
20
  module Rigor
17
21
  class Environment
@@ -32,6 +36,12 @@ module Rigor
32
36
  # See docs/internal-spec/inference-engine.md for the binding contract.
33
37
  # rubocop:disable-next Metrics/ClassLength
34
38
  class RbsLoader
39
+ # ADR-129 G4 — keyed on the class name, answered from the loader's RBS environment (or, for `class_known?`, the
40
+ # known-name set cached for it), fixed for the loader's lifetime. `@state` is not declared: beside its lazy
41
+ # derivations it holds warn-once flags and failure counters.
42
+ TypingState.memo_field(self, :@instance_definition_cache, :@singleton_definition_cache, :@class_known_cache,
43
+ reason: "keyed on the class name; a function of the loader's RBS environment")
44
+
35
45
  # Buffer name stamped on the `module` declarations synthesized by {.synthesize_missing_namespaces}.
36
46
  # Re-read off the built env by {#synthesized_namespaces} so the analysis layer can surface an `:info`
37
47
  # diagnostic naming the project's malformed-RBS namespaces — robust across the marshalled env cache,
@@ -60,13 +70,27 @@ module Rigor
60
70
  .compact.map { |root| "#{root}/".freeze }.freeze
61
71
  rescue StandardError
62
72
  [].freeze
63
- end
73
+ end.freeze
64
74
 
65
75
  # Cap on how many quarantined `signature_paths:` files {#warn_about_quarantined_signatures} lists by name
66
76
  # before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse
67
77
  # errors buries the signal.
68
78
  QUARANTINE_WARN_LIMIT = 10
69
79
 
80
+ # ADR-129 WD1 — a Frame: {.with_core_root} installs the core root for one environment build and restores the
81
+ # previous one on the way out. It changes answers (a loader built under it reads a different `core/`), and the
82
+ # installed value is a {CoreRoot.validate!}d frozen String or nil. Per thread (per fiber), so a concurrent build
83
+ # on another thread keeps its own.
84
+ CORE_ROOT = TypingState.frame(
85
+ :rbs_core_root,
86
+ reason: "installed per environment build by with_core_root; selects the core/ directory every loader of the " \
87
+ "build reads (a validated absolute path, or nil for the rbs gem's own)",
88
+ # G1′: the build installs the core root and reads it, and every analysing entry point builds the environment
89
+ # before anything types ({Environment.for_project}, `eager:`; #2883), so no `type_of` reads it.
90
+ typing_never_reads: true
91
+ )
92
+ private_constant :CORE_ROOT
93
+
70
94
  class << self
71
95
  # Issue #1902 — the fixed total order that picks ONE detail tuple for a class out of several: its two
72
96
  # sides in {#report_definition_build_failure}, or a fresh and a replayed record in
@@ -96,14 +120,40 @@ module Rigor
96
120
  # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the
97
121
  # per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
98
122
  # intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
99
- def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [])
100
- inputs = [signature_paths, virtual_rbs, deferred_signature_paths]
123
+ #
124
+ # @param project_signature_paths — issue #2422 — the project's own `signature_paths:` (not the collection,
125
+ # gem or plugin directories also on `signature_paths`): a plugin member another source declares composes
126
+ # with it as an overload unless that source is the project's own ({DeferredSignaturePlan}).
127
+ # @param core_root — issue #2517 — a validated {CoreRoot} directory loaded in place of the `rbs` gem's
128
+ # `core/`, or nil for the default core.
129
+ # @param subordinate_signature_paths — issue #2978 — the subset of `signature_paths` that loads right after
130
+ # the other `signature_paths` files and stands down member by member to them ({.add_project_signatures}).
131
+ # @param collection_signature_paths — issue #3030 — the subset of `signature_paths` that are `rbs collection`
132
+ # gem directories, read without their `_`-prefixed directories ({.project_sig_files}).
133
+ def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [], # rubocop:disable Metrics/ParameterLists
134
+ project_signature_paths: [], core_root: nil, subordinate_signature_paths: [],
135
+ collection_signature_paths: [])
136
+ inputs = [signature_paths, virtual_rbs, deferred_signature_paths, project_signature_paths,
137
+ subordinate_signature_paths, collection_signature_paths]
138
+ with_core_root(core_root) { build_env_from(libraries, inputs) }
139
+ end
140
+
141
+ # Issue #2517 — the core root {.library_loader} builds every loader of one environment build against. Held
142
+ # for the build rather than threaded through each attempt, because every trial and fallback of
143
+ # {.build_env_from} must read the same core, and a parameter missed on one of them would build that attempt
144
+ # against the default core without a sign. Thread-local, so a concurrent build on another thread keeps its
145
+ # own.
146
+ def with_core_root(core_root, &)
147
+ TypingState.current.with(CORE_ROOT, core_root, &)
148
+ end
149
+
150
+ def build_env_from(libraries, inputs)
101
151
  env, gated_dirs = whole_gated_attempt(libraries, inputs)
102
152
  return env if gated_dirs.empty?
103
153
 
104
154
  # Issue #1700 — a {RequiredFeatures}-gated directory joined; keep it whole only if it does not cost the
105
155
  # environment anything ({.gated_failures}).
106
- failed, quarantined = env ? gated_failures(env, gated_dirs, signature_paths, virtual_rbs) : [[], 1]
156
+ failed, quarantined = env ? gated_failures(env, gated_dirs, inputs) : [[], 1]
107
157
  return env if failed.empty? && quarantined.zero?
108
158
 
109
159
  tokens = gated_dirs.map { |dir| RequiredFeatures.token(RequiredFeatures.feature_for_dir(dir)) }
@@ -114,7 +164,7 @@ module Rigor
114
164
  trouble = failed.size + quarantined
115
165
  return fallback if env.nil?
116
166
 
117
- gated_trouble(fallback, gated_dirs, signature_paths, virtual_rbs) < trouble ? fallback : env
167
+ gated_trouble(fallback, gated_dirs, inputs) < trouble ? fallback : env
118
168
  end
119
169
 
120
170
  # The first attempt, with every activated gated directory whole. rbs 3.x validates a class's generic
@@ -142,7 +192,7 @@ module Rigor
142
192
  plan = GatedSignaturePlan.build(roots, fallback)
143
193
  if plan.changed
144
194
  env = gated_plan_attempt(libraries, inputs, plan)
145
- failed, quarantined = env ? gated_failures(env, gated_dirs, inputs[0], inputs[1]) : [failed, 0]
195
+ failed, quarantined = env ? gated_failures(env, gated_dirs, inputs) : [failed, 0]
146
196
  return env if env && failed.empty? && quarantined.zero?
147
197
  end
148
198
  return nil if failed.empty?
@@ -151,7 +201,7 @@ module Rigor
151
201
  env = gated_plan_attempt(libraries, inputs, plan)
152
202
  return nil if env.nil?
153
203
 
154
- failed, quarantined = gated_failures(env, gated_dirs, inputs[0], inputs[1])
204
+ failed, quarantined = gated_failures(env, gated_dirs, inputs)
155
205
  env if failed.empty? && quarantined.zero?
156
206
  end
157
207
 
@@ -171,7 +221,28 @@ module Rigor
171
221
  #
172
222
  # @param gated_plan — issue #1713 — a {GatedSignaturePlan} loaded in place of the gated directories: its
173
223
  # kept declarations where the directories would have loaded, its shells after the project's signatures.
174
- def build_env_attempt(libraries, signature_paths, virtual_rbs, deferred_signature_paths, gated_plan: nil)
224
+ #
225
+ # Issue #2470 — re-made without each {VendoredSignaturePlan} taker the attempt quarantined
226
+ # ({.quarantined_takers}), so a vendored member stands down only to a file the env keeps. Each round adds
227
+ # at least one file to the excluded set, and a round that adds none returns, so the loop ends.
228
+ def build_env_attempt(libraries, *inputs, gated_plan: nil)
229
+ excluded = Set.new
230
+ loop do
231
+ env, gated_dirs, plan = planned_env_attempt(libraries, *inputs, gated_plan: gated_plan,
232
+ vendored_excluded: excluded)
233
+ dropped = quarantined_takers(env, plan)
234
+ return [env, gated_dirs] if dropped.empty? || dropped.subset?(excluded)
235
+
236
+ excluded |= dropped
237
+ end
238
+ end
239
+
240
+ # One {.build_env_attempt} round, which also returns its {VendoredSignaturePlan}.
241
+ #
242
+ # @param vendored_excluded — signature files the vendored plan is made without.
243
+ def planned_env_attempt(libraries, signature_paths, virtual_rbs, deferred_signature_paths, # rubocop:disable Metrics/ParameterLists
244
+ project_signature_paths = [], subordinate_signature_paths = [],
245
+ collection_signature_paths = [], gated_plan: nil, vendored_excluded: Set.new)
175
246
  libraries = libraries_without_shadowed_bigdecimal_math(libraries)
176
247
  rbs_loader, loaded_libraries = library_loader(libraries)
177
248
  gated_dirs = loadable_gated_dirs(libraries, loaded_libraries)
@@ -181,15 +252,20 @@ module Rigor
181
252
  # type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06
182
253
  # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
183
254
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
184
- add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s), gated_plan ? [] : gated_dirs)
185
- env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
255
+ project_files = project_sig_files(signature_paths, collection_signature_paths)
256
+ undeferred_files = project_files - project_sig_files(deferred_signature_paths)
257
+ env, vendored_plan = bundled_env(rbs_loader, loaded_libraries, gated_plan ? [] : gated_dirs,
258
+ undeferred_files, vendored_excluded)
186
259
  gated_plan&.leading&.each { |source| add_parsed_decls(env, *source) }
187
- project_files = project_sig_files(signature_paths)
188
260
  # Issue #1075 — decided from the inputs before either side enters `env`: the consistency rule may
189
261
  # stand the `.rbs` member down, and that has to happen before its file is added.
190
- consistency = member_consistency_for(project_files, virtual_rbs)
262
+ # Issue #2422 — a bundled plugin's deferred files are not the project's: they never take part in the
263
+ # inline-vs-`.rbs` consistency rule, and stand down to an inline member like to any other source.
264
+ consistency = member_consistency_for(undeferred_files, virtual_rbs)
191
265
  add_project_signatures(env, signature_paths, deferred_signature_paths,
192
- standdowns: consistency.signature_standdowns)
266
+ standdowns: consistency.signature_standdowns,
267
+ subordinate_paths: subordinate_signature_paths,
268
+ collection_paths: collection_signature_paths)
193
269
  gated_plan&.shells&.each { |source| add_parsed_decls(env, *source) }
194
270
  # Issue #928 — the capability-role catalog goes in AFTER the project's own signatures, per
195
271
  # declaration, so a project that already declares one of these interface names keeps its own
@@ -201,16 +277,52 @@ module Rigor
201
277
  # Issues #824 / #1075 — every member a `.rbs` file also declares was ranked above; the inline side of
202
278
  # each pair the `.rbs` won stands down here ({.add_virtual_rbs}), so the two never collide.
203
279
  add_virtual_rbs(env, virtual_rbs, inline_standdowns: consistency.inline_standdowns)
280
+ # Issues #610 / #2422 — the plugins' files last, so each is planned against every other source.
281
+ add_plugin_signatures(env, deferred_signature_paths, project_signature_paths, virtual_rbs)
204
282
  synthesize_missing_namespaces(env)
205
283
  # Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
206
284
  # constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
207
285
  env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
208
- [stub_missing_referenced_types(env, resolved, project_files), gated_dirs]
286
+ [stub_missing_referenced_types(env, resolved, project_files), gated_dirs, vendored_plan]
287
+ end
288
+
289
+ # Issue #2470 — the {VendoredSignaturePlan} takers `env` does not hold: a file the build quarantined after a
290
+ # vendored member stood down to it (a `DuplicatedDeclarationError`, a class-vs-module clash). The answer reads
291
+ # only the build's inputs, so a cold, a warm and a `--no-cache` run agree.
292
+ def quarantined_takers(env, plan)
293
+ return Set.new if plan.takers.empty? || env.nil?
294
+
295
+ buffers = loaded_signature_buffers(env)
296
+ return Set.new if buffers.nil?
297
+
298
+ present = buffers.to_set { |buffer| buffer.name.to_s }
299
+ plan.takers.reject { |file| present.include?(file) }.to_set
300
+ end
301
+
302
+ # The environment of the loader's libraries and the Rigor-shipped signatures. Issue #2470 — a vendored
303
+ # directory with a member a `signature_files` file also declares loads from its {VendoredSignaturePlan},
304
+ # right after the batch, instead of whole on it.
305
+ def bundled_env(rbs_loader, loaded_libraries, gated_dirs, signature_files, excluded)
306
+ loaded_library_names = loaded_libraries.to_set(&:to_s)
307
+ plan = vendored_signature_plan(loaded_library_names, signature_files, excluded)
308
+ add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs, planned: plan.dirs)
309
+ env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
310
+ plan.sources.each { |source| add_parsed_decls(env, *source) }
311
+ [env, plan]
312
+ end
313
+
314
+ # Issue #2422 — {.add_deferred_signatures} over the plugins' files, with the buffer names of the project's
315
+ # own signature sources, which a plugin member stands down to rather than composing with: its
316
+ # `signature_paths:` files and its inline RBS.
317
+ def add_plugin_signatures(env, deferred_signature_paths, project_signature_paths, virtual_rbs)
318
+ own = project_sig_files(project_signature_paths) | virtual_rbs.to_set { |name, _| name.to_s }
319
+ add_deferred_signatures(env, project_sig_files(deferred_signature_paths), own)
209
320
  end
210
321
 
211
322
  # An `RBS::EnvironmentLoader` with every library of `libraries` that resolves added, and those libraries.
212
323
  def library_loader(libraries)
213
- rbs_loader = RBS::EnvironmentLoader.new
324
+ core_root = TypingState.current[CORE_ROOT]
325
+ rbs_loader = core_root ? RBS::EnvironmentLoader.new(core_root: Pathname(core_root)) : RBS::EnvironmentLoader.new
214
326
  # Issue #1700 — a `RequiredFeatures` token names a gated vendored directory, never an RBS library.
215
327
  loaded_libraries = libraries.select do |library|
216
328
  next false if RequiredFeatures.feature_of_token(library)
@@ -233,14 +345,16 @@ module Rigor
233
345
  # rather than a comparison of declarations because it answers exactly what RBS itself decides — an
234
346
  # overload continuation (`| ...`), a superclass written relative or absolute, a type parameter — with no
235
347
  # second reading of the rules to drift. It runs only when a gated directory joined, on an env-cache miss.
236
- def gated_trouble(env, dirs, signature_paths, virtual_rbs)
237
- failed, quarantined = gated_failures(env, dirs, signature_paths, virtual_rbs)
348
+ def gated_trouble(env, dirs, inputs)
349
+ failed, quarantined = gated_failures(env, dirs, inputs)
238
350
  failed.size + quarantined
239
351
  end
240
352
 
241
353
  # {.gated_trouble} split in two: the gated type names whose definitions fail, and the number of quarantined
242
- # signature sources that declare one. Issue #1713 re-plans against the first.
243
- def gated_failures(env, dirs, signature_paths, virtual_rbs)
354
+ # signature sources that declare one. Issue #1713 re-plans against the first. `inputs` is
355
+ # {.build_env_for}'s list: the signature paths, the virtual RBS, and (last) the collection directories.
356
+ def gated_failures(env, dirs, inputs)
357
+ signature_paths, virtual_rbs = inputs
244
358
  type_names = gated_type_names(dirs)
245
359
  builder = RBS::DefinitionBuilder.new(env: env)
246
360
  failed = type_names.select do |type_name|
@@ -257,7 +371,8 @@ module Rigor
257
371
  true
258
372
  end
259
373
  declares = gated_declaration_pattern(type_names)
260
- quarantined = collision_quarantined_project_signatures(env, signature_paths).count do |file, _note|
374
+ collided = collision_quarantined_project_signatures(env, signature_paths, inputs[5] || [])
375
+ quarantined = collided.count do |file, _note|
261
376
  File.read(file).match?(declares)
262
377
  rescue SystemCallError, IOError, ArgumentError
263
378
  false
@@ -546,12 +661,22 @@ module Rigor
546
661
  # The absolute paths of every `.rbs` file under the project's `signature_paths:` (NOT vendored /
547
662
  # stdlib RBS — those are well-formed, so attempting to build them would only waste time). Used to
548
663
  # scope the referenced-type build sweep.
549
- def project_sig_files(signature_paths)
664
+ #
665
+ # @param collection_paths — issue #3030 — the entries of `signature_paths` that are `rbs collection` gem
666
+ # directories, read without the files below a `_`-prefixed directory, as rbs reads a collection gem
667
+ # ({RbsCollectionDiscovery.hidden_file?}). Compared by spelling, as the loader receives both lists from
668
+ # one `Environment.for_project` call.
669
+ def project_sig_files(signature_paths, collection_paths = [])
670
+ collection = collection_paths.to_set(&:to_s)
550
671
  signature_paths.flat_map do |path|
551
672
  path = Pathname(path) unless path.is_a?(Pathname)
552
673
  next [] unless path.directory?
553
674
 
554
- RootedGlob.glob(path, "**/*.rbs").map { |p| File.expand_path(p) }
675
+ files = RootedGlob.glob(path, "**/*.rbs")
676
+ if collection.include?(path.to_s)
677
+ files = files.reject { |file| RbsCollectionDiscovery.hidden_file?(path, file) }
678
+ end
679
+ files.map { |p| File.expand_path(p) }
555
680
  end.to_set
556
681
  end
557
682
 
@@ -569,9 +694,16 @@ module Rigor
569
694
  # @param standdowns — issue #1075 — `{signature_path => Set[[class, method, kind]]}`, the members an
570
695
  # inline declaration displaced because it was the more precise side of a consistent pair
571
696
  # ({MemberConsistency::Resolution#signature_standdowns}). They are removed before the file is added.
572
- def add_project_signatures(env, signature_paths, deferred_paths = [], standdowns: {})
573
- deferred = project_sig_files(deferred_paths)
574
- (project_sig_files(signature_paths) - deferred).sort.each do |file|
697
+ # @param subordinate_paths — issue #2978 — the subset loaded after the rest, planned against them as a deferred
698
+ # file is ({.add_deferred_signatures}) with every one of them as `own`: a member any of them declares stands
699
+ # down from the subordinate file instead of failing the class's definition build. A gem's `rbs collection`
700
+ # copy sits behind the gem's own `sig/` this way, so `Bar#a` in both binds the gem's, and `Bar#b` in the
701
+ # copy alone still loads.
702
+ def add_project_signatures(env, signature_paths, deferred_paths = [], standdowns: {}, subordinate_paths: [],
703
+ collection_paths: [])
704
+ subordinate = project_sig_files(subordinate_paths, collection_paths) - project_sig_files(deferred_paths)
705
+ eager = project_sig_files(signature_paths, collection_paths) - project_sig_files(deferred_paths) - subordinate
706
+ eager.sort.each do |file|
575
707
  parsed = parse_signature_file(file)
576
708
  next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives
577
709
 
@@ -579,7 +711,7 @@ module Rigor
579
711
  strip_members!(decls, standdowns[file]) if standdowns.key?(file)
580
712
  add_project_parsed_decls(env, buffer, directives, decls)
581
713
  end
582
- add_deferred_signatures(env, deferred, standdowns)
714
+ add_deferred_signatures(env, subordinate, eager, standdowns: standdowns)
583
715
  end
584
716
 
585
717
  # Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
@@ -617,16 +749,26 @@ module Rigor
617
749
  # (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
618
750
  # exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
619
751
  # innocent ones is the change to make if that ever stops being true.
620
- def add_deferred_signatures(env, deferred_files, standdowns = {})
752
+ #
753
+ # Issue #2422 — at equal arity the plugin reopens the class, and a method both sources declare fails the
754
+ # definition build (`DuplicatedMethodDefinitionError`) for the class and everything under it; a constant
755
+ # both declare raises `DuplicatedDeclarationError` and drops the plugin's whole file. So the file is planned
756
+ # against the env before it is added ({DeferredSignaturePlan}): a colliding method composes with another
757
+ # source's `def` as an overload continuation, or stands down when the other source is the project's own
758
+ # (`own`, buffer names) or cannot be continued; a colliding declaration stands down on its own; the rest
759
+ # of the file loads. {.deferred_member_standdowns} reads what stood down back off the built env.
760
+ #
761
+ # @param standdowns — issue #2978 — {.add_project_signatures}' members to remove before a file is planned.
762
+ def add_deferred_signatures(env, deferred_files, own = Set.new, standdowns: {})
621
763
  deferred_files.sort.each do |file|
622
764
  parsed = parse_signature_file(file)
623
765
  next if parsed.nil?
624
766
 
625
767
  buffer, directives, decls = parsed
768
+ strip_members!(decls, standdowns[file]) if standdowns.key?(file)
626
769
  next if generic_arity_conflict(env, decls)
627
770
 
628
- strip_members!(decls, standdowns[file]) if standdowns.key?(file)
629
- add_project_parsed_decls(env, buffer, directives, decls)
771
+ add_project_parsed_decls(env, buffer, directives, DeferredSignaturePlan.plan(decls, env, own))
630
772
  end
631
773
  end
632
774
 
@@ -648,6 +790,24 @@ module Rigor
648
790
  end
649
791
  end
650
792
 
793
+ # Issue #2422 — the members {.add_deferred_signatures} left out of each deferred file it loaded, as
794
+ # `[absolute_path, declaration, nil, nil, cause_file, :member]` — `declaration` reading `Object#acts_like?`
795
+ # or `Time.at`, `cause_file` the buffer of the source that declares it, or nil when that cannot be named.
796
+ # Derived from the built env for {.deferred_standdowns}' reason; a file that stood down whole is
797
+ # {.deferred_standdowns}' row, and one the env does not hold has nothing to read back.
798
+ def deferred_member_standdowns(env, deferred_paths)
799
+ return [] if env.nil? || deferred_paths.nil? || deferred_paths.empty?
800
+
801
+ loaded = loaded_signature_buffers(env)&.to_set { |buffer| buffer.name.to_s } || Set.new
802
+ project_sig_files(deferred_paths).sort.flat_map do |file|
803
+ parsed = loaded.include?(file) ? parse_signature_file(file) : nil
804
+ next [] if parsed.nil? || generic_arity_conflict(env, parsed[2])
805
+
806
+ rows = DeferredSignaturePlan.standdown_rows(env, file, parsed[2]).sort_by(&:first)
807
+ rows.map { |label, cause| [file, label, nil, nil, cause, :member] }
808
+ end
809
+ end
810
+
651
811
  # The first `[class_name, existing_arity, incoming_arity, existing_file]` where `decls` would
652
812
  # re-declare a class the env already has with a DIFFERENT number of type parameters, or nil when
653
813
  # they agree everywhere. Equal arity is not a conflict: RBS reopens the class, which is the ordinary
@@ -690,9 +850,9 @@ module Rigor
690
850
  decls.each do |decl|
691
851
  case decl
692
852
  when ::RBS::AST::Declarations::Module
693
- each_declared_class(decl.members, prefix + [decl.name.to_s], &block)
853
+ each_declared_class(decl.members, declared_class_path(decl, prefix), &block)
694
854
  when ::RBS::AST::Declarations::Class
695
- inner = prefix + [decl.name.to_s]
855
+ inner = declared_class_path(decl, prefix)
696
856
  block.call("::#{inner.join('::')}", decl.type_params.size)
697
857
  each_declared_class(decl.members, inner, &block)
698
858
  end
@@ -733,8 +893,8 @@ module Rigor
733
893
  # {.add_project_signatures} so the warning fires even on a cache hit (where the env was already built
734
894
  # with the file quarantined). Cheap: it only re-parses the user's own (usually small) `sig/` set, and
735
895
  # returns empty immediately when there is no `signature_paths:`.
736
- def quarantined_project_signatures(signature_paths)
737
- project_sig_files(signature_paths).sort.filter_map do |file|
896
+ def quarantined_project_signatures(signature_paths, collection_paths = [])
897
+ project_sig_files(signature_paths, collection_paths).sort.filter_map do |file|
738
898
  # The note carries the path itself because the warn composer prints only this element — a
739
899
  # `ParsingError` message embeds its `path:line:` prefix, so the composer never adds one.
740
900
  content = File.read(file, encoding: "UTF-8")
@@ -765,7 +925,7 @@ module Rigor
765
925
  COLLISION_QUARANTINE_NOTE =
766
926
  "duplicated declaration against bundled RBS — quarantined so the rest of the RBS env still loads"
767
927
 
768
- def collision_quarantined_project_signatures(env, signature_paths)
928
+ def collision_quarantined_project_signatures(env, signature_paths, collection_paths = [])
769
929
  return [] if env.nil?
770
930
 
771
931
  present = env.buffers.to_set do |buffer|
@@ -777,7 +937,7 @@ module Rigor
777
937
  name.to_s
778
938
  end
779
939
  buffers_by_basename = env.buffers.group_by { |buffer| File.basename(buffer.name.to_s) }
780
- project_sig_files(signature_paths).sort.filter_map do |file|
940
+ project_sig_files(signature_paths, collection_paths).sort.filter_map do |file|
781
941
  next if present.include?(file)
782
942
  next if parse_signature_file(file).nil? # parse / encoding quarantine owns these
783
943
  next if project_signature_loaded_under_other_path?(file, buffers_by_basename)
@@ -1133,11 +1293,12 @@ module Rigor
1133
1293
  end
1134
1294
 
1135
1295
  # `[owners, shadowable]`. `owners` is `{[class_name, method_name, kind] => [signature_path, member,
1136
- # visibility]}` for every method-shaped member the project's own `signature_paths:` files declare and
1137
- # the inline sources also name; the first file in sorted order owns a key two `.rbs` files declare
1138
- # (that pair fails the definition build anyway). `shadowable` is every `::` segment of every class /
1139
- # module name the inline sources and the signature files declare — the names a relative reference
1140
- # might resolve to inside an enclosing namespace ({MemberConsistency::Comparator#faithful?}).
1296
+ # visibility]}` for every method-shaped or instance-variable member the project's own
1297
+ # `signature_paths:` files declare and the inline sources also name; the first file in sorted order
1298
+ # owns a key two `.rbs` files declare (that pair fails the definition build anyway). `shadowable` is
1299
+ # every `::` segment of every class / module name the inline sources and the signature files declare
1300
+ # — the names a relative reference might resolve to inside an enclosing namespace
1301
+ # ({MemberConsistency::Comparator#faithful?}).
1141
1302
  #
1142
1303
  # The parse is scoped by a substring pre-filter over each file's text: a declaration has to spell its
1143
1304
  # own name, so a signature file mentioning none of the names the inline sources declare cannot
@@ -1163,14 +1324,14 @@ module Rigor
1163
1324
  def index_signature_members(index, file, decls)
1164
1325
  return if decls.nil?
1165
1326
 
1166
- each_declared_member(decls) do |class_name, member, visibility|
1167
- member_method_keys(member).each do |method_name, kind|
1327
+ each_declared_member(decls) do |class_name, member, visibility, nesting|
1328
+ member_keys(member, nesting).each do |method_name, kind|
1168
1329
  index[[class_name, method_name, kind]] ||= [file, member, visibility].freeze
1169
1330
  end
1170
1331
  end
1171
1332
  end
1172
1333
 
1173
- # The directive heads a refinement {MemberConsistency.refinement_conflicts} checks, used to skip
1334
+ # The directive heads a refinement {MemberConsistency::Comparator#refinement_conflicts} checks, used to skip
1174
1335
  # parsing an inline source that carries none when no `.rbs` overlaps it. Narrower than `rigor:v1:` on
1175
1336
  # purpose: rbs-inline's output marks every skeleton `%a{rigor:v1:inferred-return}`, so that prefix is
1176
1337
  # in nearly every synthesized source.
@@ -1190,49 +1351,65 @@ module Rigor
1190
1351
  decls = parse_signature_source(name, content)
1191
1352
  next if decls.nil?
1192
1353
 
1193
- each_declared_member(decls) do |class_name, member, visibility|
1194
- entries << [name.to_s, class_name, member, visibility]
1354
+ each_declared_member(decls) do |class_name, member, visibility, nesting|
1355
+ entries << [name.to_s, class_name, member, visibility, nesting]
1195
1356
  end
1196
1357
  end
1197
1358
  end
1198
1359
 
1199
- # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`).
1200
- # Nested declarations are descended into after the rejection, and a declaration member contributes no
1201
- # method key, so nesting is never itself removed.
1202
- def strip_members!(decls, keys, prefix = [])
1360
+ # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`,
1361
+ # instance-variable keys included, #2440). Nested declarations are descended into after the
1362
+ # rejection, and a declaration member contributes no key, so nesting is never itself removed.
1363
+ def strip_members!(decls, keys, prefix = [], written = [])
1203
1364
  Array(decls).each do |decl|
1204
1365
  next unless declaration_with_members?(decl)
1205
1366
 
1206
- inner = prefix + [decl.name.to_s.delete_prefix("::")]
1367
+ inner = declared_class_path(decl, prefix)
1368
+ nesting = written + [decl.name.to_s]
1207
1369
  class_name = inner.join("::")
1208
1370
  decl.members.reject! do |member|
1209
- member_method_keys(member).any? { |method_name, kind| keys.include?([class_name, method_name, kind]) }
1371
+ member_keys(member, nesting.join("/")).any? do |method_name, kind|
1372
+ keys.include?([class_name, method_name, kind])
1373
+ end
1210
1374
  end
1211
- strip_members!(decl.members, keys, inner)
1375
+ strip_members!(decl.members, keys, inner, nesting)
1212
1376
  end
1213
1377
  end
1214
1378
 
1215
- # Yields `[class_name, member, visibility]` for every member of every class / module declaration in
1216
- # `decls`, descending through nesting. The enclosing path is accumulated and the name rendered
1217
- # `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is
1218
- # what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the member's
1219
- # own (`private def x`) or else the one the enclosing `private` / `public` section sets; a singleton
1220
- # method takes no section, as in Ruby.
1221
- def each_declared_member(decls, prefix = [], &block)
1379
+ # The class path a declaration contributes to member keys: its name `::`-stripped, under the enclosing
1380
+ # path unless the name is absolute. A leading `::` resets the path, as RBS resolves `module A; class
1381
+ # ::Abs` to the top-level `Abs` (issue #2474), and so do the children of that declaration. The
1382
+ # `nesting` a mixin is keyed by is not reset: it keeps the names as written, which is the lexical
1383
+ # scope a relative mixin name resolves through.
1384
+ def declared_class_path(decl, prefix)
1385
+ base = decl.name.to_s.start_with?("::") ? [] : prefix
1386
+ base + [decl.name.to_s.delete_prefix("::")]
1387
+ end
1388
+
1389
+ # Yields `[class_name, member, visibility, nesting]` for every member of every class / module
1390
+ # declaration in `decls`, descending through nesting. The enclosing path is accumulated and the name
1391
+ # rendered `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same —
1392
+ # which is what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the
1393
+ # member's own (`private def x`) or else the one the enclosing `private` / `public` section sets; a
1394
+ # singleton method takes no section, as in Ruby. `nesting` is the declaration names exactly as
1395
+ # written, outermost first, joined by `/` (`Foo/Bar` against `Foo::Bar`): a mixin's relative name
1396
+ # resolves through it, so {MemberConsistency.member_keys} keys a mixin by it (#2440).
1397
+ def each_declared_member(decls, prefix = [], written = [], &block)
1222
1398
  Array(decls).each do |decl|
1223
1399
  next unless declaration_with_members?(decl)
1224
1400
 
1225
- inner = prefix + [decl.name.to_s.delete_prefix("::")]
1401
+ inner = declared_class_path(decl, prefix)
1402
+ nesting = written + [decl.name.to_s]
1226
1403
  class_name = inner.join("::")
1227
1404
  section = :public
1228
1405
  decl.members.each do |member|
1229
1406
  case member
1230
1407
  when ::RBS::AST::Members::Private then section = :private
1231
1408
  when ::RBS::AST::Members::Public then section = :public
1232
- else block.call(class_name, member, member_visibility(member, section))
1409
+ else block.call(class_name, member, member_visibility(member, section), nesting.join("/"))
1233
1410
  end
1234
1411
  end
1235
- each_declared_member(decl.members, inner, &block)
1412
+ each_declared_member(decl.members, inner, nesting, &block)
1236
1413
  end
1237
1414
  end
1238
1415
 
@@ -1254,6 +1431,12 @@ module Rigor
1254
1431
  MemberConsistency.member_method_keys(member)
1255
1432
  end
1256
1433
 
1434
+ # {MemberConsistency.member_keys} — the method keys plus an instance variable's and a mixin's (#2440),
1435
+ # which is what the inline-vs-`sig/` consistency rule indexes and strips by.
1436
+ def member_keys(member, nesting = nil)
1437
+ MemberConsistency.member_keys(member, nesting)
1438
+ end
1439
+
1257
1440
  # A class / module declaration header, as the pre-filter in {.signature_member_index} reads it out of
1258
1441
  # synthesized RBS text.
1259
1442
  INLINE_DECLARATION_NAME = /^[ \t]*(?:class|module)[ \t]+([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)/
@@ -1402,7 +1585,8 @@ module Rigor
1402
1585
  # add self-contained declarations) and stay unconditional.
1403
1586
  LIBRARY_SUPPLEMENT_CORE_OVERLAYS = {
1404
1587
  "resolv.rbs" => "resolv",
1405
- "string_scanner.rbs" => "strscan"
1588
+ "string_scanner.rbs" => "strscan",
1589
+ "bigdecimal.rbs" => "bigdecimal"
1406
1590
  }.freeze
1407
1591
  private_constant :LIBRARY_SUPPLEMENT_CORE_OVERLAYS
1408
1592
 
@@ -1428,8 +1612,12 @@ module Rigor
1428
1612
  #
1429
1613
  # @param loaded_library_names — libraries that actually resolved on this loader.
1430
1614
  # @param gated_dirs — the {RequiredFeatures}-gated directory basenames this library list activates.
1431
- def add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs = [])
1615
+ #
1616
+ # @param planned — issue #2470 — the {VendoredSignaturePlan} directories, which load from the plan instead.
1617
+ def add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs = [], planned: [])
1432
1618
  vendored_gem_sig_paths.each do |path|
1619
+ next if planned.include?(path)
1620
+
1433
1621
  rbs_loader.add(path: path) if vendored_dir_loads?(path, loaded_library_names, gated_dirs)
1434
1622
  end
1435
1623
  core_overlay_sig_paths.each do |dir|
@@ -1445,6 +1633,18 @@ module Rigor
1445
1633
  end
1446
1634
  end
1447
1635
 
1636
+ # Issue #2470 — the ungated vendored directories this build loads, planned against the signature files
1637
+ # that outrank them ({VendoredSignaturePlan}). The gated ones have their own plan (#1713).
1638
+ #
1639
+ # @param signature_files — the `signature_paths:` files a bundled plugin did not contribute: the project's
1640
+ # own, a collection's and a gem's. A plugin's files are planned against the env instead (#2422).
1641
+ def vendored_signature_plan(loaded_library_names, signature_files, excluded = Set.new)
1642
+ roots = vendored_gem_sig_paths.select do |path|
1643
+ !RequiredFeatures.gated_dir?(path.basename.to_s) && vendored_dir_loads?(path, loaded_library_names, [])
1644
+ end
1645
+ VendoredSignaturePlan.build(roots, signature_files, excluded)
1646
+ end
1647
+
1448
1648
  # Issue #1700 — the {RequiredFeatures}-gated directories `libraries` activates, less one whose feature
1449
1649
  # resolved as an RBS library here (`libraries: [prime]` on a host whose `prime` gem ships its `sig/`),
1450
1650
  # whose own copy is then already loaded. {.build_env_for} then keeps a directory that joined only if the
@@ -1487,17 +1687,19 @@ module Rigor
1487
1687
  end
1488
1688
 
1489
1689
  # Rigor-owned per-gem RBS overlays (`data/gem_overlay/<gem>/`), ADR-72. Unlike the unconditional
1490
- # `core_overlay`, each gem's overlay is loaded ONLY when that gem is locked in the project's
1491
- # Gemfile.lock but ships no RBS of its own — {Environment.for_project} decides eligibility and passes
1492
- # the already-filtered gem-name set here. One directory per gem name keeps the membership check a
1493
- # cheap `File.directory?`.
1690
+ # `core_overlay`, each gem's overlay is loaded only when that gem is locked in the project's
1691
+ # Gemfile.lock AND {Environment.for_project} finds its coverage source overlay-eligible
1692
+ # ({Environment::GEM_OVERLAY_COVERAGE_SOURCES} — `:missing` for every overlay but addressable's
1693
+ # `:rbs_collection` correction, #2857) with no reachable plugin twin; this method receives the
1694
+ # already-filtered gem-name set. One directory per gem name keeps the membership check a cheap
1695
+ # `File.directory?`.
1494
1696
  GEM_OVERLAY_SIGS_ROOT = File.expand_path(
1495
1697
  "../../../data/gem_overlay",
1496
1698
  __dir__
1497
1699
  ).freeze
1498
1700
 
1499
- # @param gem_names — overlay-eligible Gemfile.lock gem names (the caller filters
1500
- # to the `:missing`-coverage, no-conflicting-plugin set).
1701
+ # @param gem_names — overlay-eligible Gemfile.lock gem names (the caller filters to the
1702
+ # coverage-source-eligible, no-conflicting-plugin set).
1501
1703
  # @return the bundled overlay directory for each gem that ships one; empty when
1502
1704
  # none match or the overlay root is absent.
1503
1705
  def gem_overlay_sig_paths(gem_names)
@@ -1511,13 +1713,16 @@ module Rigor
1511
1713
 
1512
1714
  # @param path — typically an entry from an {RbsLoader} instance's
1513
1715
  # {#signature_paths}.
1514
- # @return whether `path` sits under the bundled gem-overlay root — what
1515
- # `CheckRules::GEM_OVERLAY_OPEN_RECEIVERS`'s gate consults so that a class name's membership in
1516
- # that list alone never grants the open-receiver exemption; the overlay directory that made the
1517
- # entry true must have actually loaded THIS run too (issue #632, tracked further by #660).
1518
- # `GEM_OVERLAY_SIGS_ROOT` is defined inside this `class << self` block, so it is reachable from
1519
- # here directly but NOT as `RbsLoader::GEM_OVERLAY_SIGS_ROOT` from outside — this method is the
1520
- # public seam callers outside this class use instead of reaching for the constant themselves.
1716
+ # @return whether `path` sits under the bundled gem-overlay root — the prefix half of
1717
+ # "this run loaded a `data/gem_overlay/` directory". Callers needing ONE gem's overlay ask
1718
+ # {.gem_overlay_sig_paths} for that gem's directory instead: `signature_standdowns` uses this
1719
+ # to recognise overlay rows across every gem, while `CheckRules#gem_overlay_loaded?` keys on
1720
+ # the activesupport overlay alone (#3041 — a project can load the addressable overlay without
1721
+ # the activesupport one, and only the latter may open `ActiveSupport::Duration` /
1722
+ # `ActiveSupport::Concern`). `GEM_OVERLAY_SIGS_ROOT` is defined inside this `class << self`
1723
+ # block, so it is reachable from here directly but NOT as `RbsLoader::GEM_OVERLAY_SIGS_ROOT`
1724
+ # from outside — this method is the public seam callers outside this class use instead of
1725
+ # reaching for the constant themselves.
1521
1726
  def under_gem_overlay_root?(path)
1522
1727
  path.to_s.start_with?("#{GEM_OVERLAY_SIGS_ROOT}/")
1523
1728
  end
@@ -1632,7 +1837,8 @@ module Rigor
1632
1837
  end
1633
1838
  end
1634
1839
 
1635
- attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths
1840
+ attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths,
1841
+ :project_signature_paths, :core_root, :subordinate_signature_paths, :collection_signature_paths
1636
1842
 
1637
1843
  # @param libraries — stdlib library names to load on top of core (e.g.,
1638
1844
  # `["pathname", "json"]`). Empty by default. Each entry MUST correspond to a directory under the
@@ -1645,7 +1851,7 @@ module Rigor
1645
1851
  # `RbsEnvironment`, `RbsKnownClassNames`, `RbsConstantTable`, `RbsClassAncestorTable`, and
1646
1852
  # `RbsClassTypeParamNames` producers. Pass `nil` (the default) to skip caching; the runner threads
1647
1853
  # its own Store through here when enabled.
1648
- # @param virtual_rbs — >] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1854
+ # @param virtual_rbs — ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1649
1855
  # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
1650
1856
  # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
1651
1857
  # synthesizer-emitting plugin is loaded.
@@ -1653,15 +1859,22 @@ module Rigor
1653
1859
  # contributed (issue #610): loaded last and allowed to stand down against a colliding generic
1654
1860
  # arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
1655
1861
  # a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
1656
- def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
1657
- deferred_signature_paths: [], project_signature_paths: [], required_features: [])
1862
+ # @param core_root — issue #2517 — a directory loaded in place of the `rbs` gem's `core/`, or nil (the
1863
+ # default) for that core. Validated here, so a bad selection fails at construction ({CoreRoot.validate!}).
1864
+ # @param subordinate_signature_paths — issue #2978 — the subset of `signature_paths:` that stands down member by
1865
+ # member to the rest of it (a bundle gem's `rbs collection` copy, behind the gem's own `sig/`).
1866
+ # @param collection_signature_paths — issue #3030 — the subset of `signature_paths:` that are `rbs collection`
1867
+ # gem directories, read without the files below their `_`-prefixed directories ({.project_sig_files}).
1868
+ def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [], # rubocop:disable Metrics/ParameterLists
1869
+ deferred_signature_paths: [], project_signature_paths: [], required_features: [], core_root: nil,
1870
+ subordinate_signature_paths: [], collection_signature_paths: [])
1871
+ @core_root = core_root.nil? ? nil : CoreRoot.validate!(core_root)
1872
+ CoreRoot.reject_overlap!(@core_root, project_signature_paths) if @core_root
1658
1873
  @libraries = libraries.map(&:to_s).freeze
1659
1874
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
1660
1875
  assign_reopen_inputs(project_signature_paths, required_features)
1661
- # Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
1662
- # allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
1663
- # is also in `@signature_paths`, so the env cache descriptor already digests these files.
1664
- @deferred_signature_paths = deferred_signature_paths.map { |p| Pathname(p) }.freeze
1876
+ assign_partition(deferred_signature_paths, subordinate_signature_paths)
1877
+ @collection_signature_paths = collection_signature_paths.map { |p| Pathname(p) }.freeze
1665
1878
  @cache_store = cache_store
1666
1879
  @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
1667
1880
  # Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be
@@ -1702,9 +1915,9 @@ module Rigor
1702
1915
  # @return empty when every `signature_paths:` file parses.
1703
1916
  def quarantined_signatures
1704
1917
  @state[:quarantined] ||= begin
1705
- parse_quarantined = self.class.quarantined_project_signatures(@signature_paths)
1918
+ parse_quarantined = self.class.quarantined_project_signatures(@signature_paths, @collection_signature_paths)
1706
1919
  collision_quarantined = self.class.collision_quarantined_project_signatures(
1707
- @state[:env], @signature_paths
1920
+ @state[:env], @signature_paths, @collection_signature_paths
1708
1921
  )
1709
1922
  parse_paths = parse_quarantined.to_set { |path, _note| path }
1710
1923
  # Issue #610 — a deferred file that STOOD DOWN is absent from the env by design, which is exactly
@@ -1725,8 +1938,17 @@ module Rigor
1725
1938
  # plugin sig collides, which is every project that has not run `rbs collection install` for a gem a
1726
1939
  # bundled plugin also declares. Reported as `rbs.coverage.plugin-signature-stood-down` (`:info`); the
1727
1940
  # same file MUST NOT also appear in {#quarantined_signatures}.
1941
+ #
1942
+ # Issue #2422 — followed by the members a deferred file that did load left out because another source
1943
+ # declares them ({.deferred_member_standdowns}), one row each.
1944
+ #
1945
+ # Issue #2974 — a gem overlay's rows are left out: the overlay is Rigor's own default, deferred so it stands
1946
+ # down to any source that declares the same thing, and that is not news to report (#2470's reasoning).
1728
1947
  def signature_standdowns
1729
- @state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
1948
+ @state[:signature_standdowns] ||= (
1949
+ self.class.deferred_standdowns(env, @deferred_signature_paths) +
1950
+ self.class.deferred_member_standdowns(env, @deferred_signature_paths)
1951
+ ).reject { |row| self.class.under_gem_overlay_root?(row.first) }.freeze
1730
1952
  end
1731
1953
 
1732
1954
  # Issue #1713 — what stood down from each {RequiredFeatures}-gated vendored directory this loader's library
@@ -1745,7 +1967,7 @@ module Rigor
1745
1967
  end
1746
1968
  return [] if roots.empty?
1747
1969
 
1748
- GatedSignaturePlan.standdowns(env, roots, self.class.project_sig_files(@signature_paths))
1970
+ GatedSignaturePlan.standdowns(env, roots, signature_files)
1749
1971
  end
1750
1972
  private :derive_vendored_standdowns
1751
1973
 
@@ -1781,7 +2003,7 @@ module Rigor
1781
2003
  environment = env
1782
2004
  return Set.new.freeze if environment.nil?
1783
2005
 
1784
- project_files = self.class.project_sig_files(@signature_paths)
2006
+ project_files = signature_files
1785
2007
  virtual_names = @virtual_rbs.to_set(&:first)
1786
2008
  return Set.new.freeze if project_files.empty? && virtual_names.empty?
1787
2009
 
@@ -1851,6 +2073,22 @@ module Rigor
1851
2073
  # Issue #2174 — the subset of `signature_paths:` the user configured (every entry is also in
1852
2074
  # `@signature_paths`), and the features the project's source requires: read only by
1853
2075
  # {#ruby_library_reopen?}.
2076
+ # Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and allowed to stand
2077
+ # down against a colliding generic arity ({.add_deferred_signatures}). Issue #2978 — and the subset that stands
2078
+ # down member by member to the rest. Every entry of either is also in `@signature_paths`, so the env cache
2079
+ # descriptor already digests these files.
2080
+ # Issue #3030 — every `.rbs` file this loader reads from its `signature_paths:`, the `_`-prefixed directories of
2081
+ # its `rbs collection` gem directories left out ({.project_sig_files}).
2082
+ def signature_files
2083
+ self.class.project_sig_files(@signature_paths, @collection_signature_paths)
2084
+ end
2085
+ private :signature_files
2086
+
2087
+ def assign_partition(deferred_signature_paths, subordinate_signature_paths)
2088
+ @deferred_signature_paths = deferred_signature_paths.map { |p| Pathname(p) }.freeze
2089
+ @subordinate_signature_paths = subordinate_signature_paths.map { |p| Pathname(p) }.freeze
2090
+ end
2091
+
1854
2092
  def assign_reopen_inputs(project_signature_paths, required_features)
1855
2093
  @project_signature_paths = project_signature_paths.map { |p| Pathname(p) }.freeze
1856
2094
  @required_features = required_features.map { |feature| feature.to_s.dup.freeze }.freeze
@@ -1858,6 +2096,62 @@ module Rigor
1858
2096
  private :assign_reopen_inputs
1859
2097
  private :ruby_library_roots
1860
2098
 
2099
+ # Issue #2438 — true when every RBS declaration of `class_name` is the project's own
2100
+ # ({#project_authored_classes}) and none of them declares a member of the class itself: no method, attribute,
2101
+ # alias or mixin, only nested class / module / constant / type declarations (and variable or visibility
2102
+ # lines, which add no method). `rigor sig-gen --write` writes exactly this for a namespace whose source
2103
+ # lists no methods of its own (`module NS; class Foo; …; end; end`), while the namespace's methods live
2104
+ # where the generator cannot see them: a file outside `paths:`, a mixin, a hook. Such a declaration
2105
+ # names the class without describing its surface. Bundled (core, stdlib, gem) declarations never count.
2106
+ def memberless_project_class?(class_name)
2107
+ memberless_project_classes.key?(class_name.to_s.delete_prefix("::"))
2108
+ end
2109
+
2110
+ # The superclass a member-less project declaration of `class_name` writes (`class Poll < ::ApplicationRecord`),
2111
+ # as RBS spells it (rooted), or nil when none of its declarations writes one or the class is not member-less.
2112
+ def memberless_project_superclass(class_name)
2113
+ memberless_project_classes[class_name.to_s.delete_prefix("::")]
2114
+ end
2115
+
2116
+ def memberless_project_classes
2117
+ @state[:memberless_project_classes] ||= build_memberless_project_classes
2118
+ end
2119
+ private :memberless_project_classes
2120
+
2121
+ SURFACE_MEMBERS = [
2122
+ ::RBS::AST::Members::MethodDefinition, ::RBS::AST::Members::Attribute,
2123
+ ::RBS::AST::Members::Alias, ::RBS::AST::Members::Mixin
2124
+ ].freeze
2125
+ private_constant :SURFACE_MEMBERS
2126
+
2127
+ def build_memberless_project_classes
2128
+ authored = project_authored_classes
2129
+ environment = env
2130
+ return {}.freeze if authored.empty? || environment.nil?
2131
+
2132
+ environment.class_decls.each_with_object({}) do |(rbs_name, entry), acc|
2133
+ name = rbs_name.to_s.delete_prefix("::")
2134
+ next unless authored.include?(name)
2135
+
2136
+ decls = self.class.entry_declarations(entry)
2137
+ acc[name] = declared_superclass_name(decls) if decls.none? { |decl| surface_member?(decl) }
2138
+ end.freeze
2139
+ rescue ::RBS::BaseError
2140
+ {}.freeze
2141
+ end
2142
+
2143
+ def surface_member?(decl)
2144
+ members = decl.respond_to?(:members) ? decl.members : []
2145
+ members.any? { |member| SURFACE_MEMBERS.any? { |kind| member.is_a?(kind) } }
2146
+ end
2147
+ private :surface_member?
2148
+
2149
+ def declared_superclass_name(decls)
2150
+ decls.filter_map { |decl| decl.respond_to?(:super_class) && decl.super_class&.name&.to_s }.first&.freeze
2151
+ end
2152
+ private :declared_superclass_name
2153
+ private :build_memberless_project_classes
2154
+
1861
2155
  # The class / module names every one of whose declarations is the project's own
1862
2156
  # ({#project_authored_entry?}, which {#inline_only_entry?} asks behind its inline-only gate).
1863
2157
  def project_authored_classes
@@ -1907,17 +2201,33 @@ module Rigor
1907
2201
  # the interpreter implements, as opposed to a stdlib library written in Ruby (`Logger` rewrites `@level` in
1908
2202
  # its `initialize`). Same attribution, same cache behaviour.
1909
2203
  def core_class?(class_name)
1910
- (@state[:core_classes] ||= build_core_stdlib_classes(CORE_ROOTS)).include?(class_name.to_s.delete_prefix("::"))
2204
+ (@state[:core_classes] ||= build_core_stdlib_classes(core_roots)).include?(class_name.to_s.delete_prefix("::"))
2205
+ end
2206
+
2207
+ # Issue #2517 — the `core/` prefixes a declaration's buffer name is tested against: the selected
2208
+ # {#core_root} when there is one, which is what the environment loaded core from, else the `rbs` gem's.
2209
+ def core_roots
2210
+ @core_root ? ["#{@core_root}/".freeze].freeze : CORE_ROOTS
1911
2211
  end
2212
+ private :core_roots
2213
+
2214
+ # {CORE_STDLIB_ROOTS} with {#core_roots} in place of the `rbs` gem's `core/`.
2215
+ def core_stdlib_roots
2216
+ return CORE_STDLIB_ROOTS unless @core_root
2217
+ return [].freeze if CORE_STDLIB_ROOTS.empty?
2218
+
2219
+ (core_roots + (CORE_STDLIB_ROOTS - CORE_ROOTS)).freeze
2220
+ end
2221
+ private :core_stdlib_roots
1912
2222
 
1913
2223
  CORE_ROOTS = begin
1914
2224
  [::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT].compact.map { |root| "#{root}/".freeze }.freeze
1915
2225
  rescue StandardError
1916
2226
  [].freeze
1917
- end
2227
+ end.freeze
1918
2228
  private_constant :CORE_ROOTS
1919
2229
 
1920
- def build_core_stdlib_classes(roots = CORE_STDLIB_ROOTS)
2230
+ def build_core_stdlib_classes(roots = core_stdlib_roots)
1921
2231
  environment = env
1922
2232
  return Set.new.freeze if environment.nil? || roots.empty?
1923
2233
 
@@ -1941,6 +2251,96 @@ module Rigor
1941
2251
  core_stdlib_classes.include?(class_name.to_s.delete_prefix("::"))
1942
2252
  end
1943
2253
 
2254
+ # Issue #2884 — true when every declaration of `method_definition` (an `RBS::Definition::Method`) is in a named
2255
+ # buffer outside the `rbs` gem's `core/` and `stdlib/` trees: a project, gem or collection signature wrote it.
2256
+ # Attributed by buffer name, as {#core_or_stdlib_class?} is; a declaration whose buffer name is lost (an old
2257
+ # environment blob's `<cached>` sentinel) answers false, which switches the caller's feature off.
2258
+ def declared_outside_core_stdlib?(method_definition)
2259
+ roots = core_stdlib_roots
2260
+ type_defs = method_definition.defs
2261
+ return false if roots.empty? || type_defs.empty?
2262
+
2263
+ type_defs.all? do |type_def|
2264
+ name = self.class.declaration_buffer_name(type_def.member)
2265
+ !name.nil? && roots.none? { |root| name.start_with?(root) }
2266
+ end
2267
+ end
2268
+
2269
+ # Issue #2884 — true when `method_definition`, a class's singleton `new`, is one `X.new` answers through: every
2270
+ # definition is an explicit `def self.new` (not the `new` `RBS::DefinitionBuilder` derives from `initialize`,
2271
+ # which returns the declaring class), written outside core / stdlib ({#declared_outside_core_stdlib?}), on a
2272
+ # class core / stdlib does not declare (a project signature reopening `String` does not change `String.new`).
2273
+ def honoured_singleton_new?(method_definition)
2274
+ method_definition.defs.all? { |type_def| explicit_singleton_new_member?(type_def.member) } &&
2275
+ declared_outside_core_stdlib?(method_definition) &&
2276
+ !core_or_stdlib_class?(method_definition.defined_in.to_s)
2277
+ end
2278
+
2279
+ def explicit_singleton_new_member?(member)
2280
+ member.is_a?(::RBS::AST::Members::MethodDefinition) && member.name == :new && member.kind != :instance
2281
+ end
2282
+ private :explicit_singleton_new_member?
2283
+
2284
+ # Issue #2884 — true when `method_definition`, a singleton `new`, answers what `Nominal[receiver]` answers for
2285
+ # every receiver that inherits it: each overload returns `instance`, untyped, `top`, `bot` or `void`, or the
2286
+ # declaring class itself, compared by its resolved full name. `self` is the receiver's class object, not its
2287
+ # instance, so it does not count. The dispatcher answers `Nominal[receiver]` for an inherited declaration this
2288
+ # holds of, and {#singleton_new_departs_from_instance?} is built from the same test, so the two cannot drift.
2289
+ def answers_instance_for_subclass?(method_definition)
2290
+ owner = method_definition.defined_in.to_s.delete_prefix("::")
2291
+ method_definition.method_types.all? do |method_type|
2292
+ returned = method_type.type.return_type
2293
+ INSTANCE_LIKE_RETURNS.any? { |base| returned.is_a?(base) } ||
2294
+ (returned.is_a?(::RBS::Types::ClassInstance) && returned.name.to_s.delete_prefix("::") == owner)
2295
+ end
2296
+ end
2297
+
2298
+ INSTANCE_LIKE_RETURNS = [
2299
+ ::RBS::Types::Bases::Instance, ::RBS::Types::Bases::Any, ::RBS::Types::Bases::Top,
2300
+ ::RBS::Types::Bases::Bottom, ::RBS::Types::Bases::Void
2301
+ ].freeze
2302
+ private_constant :INSTANCE_LIKE_RETURNS
2303
+
2304
+ # Issue #2884 — true when some class declares a {#honoured_singleton_new?} `new` that is not
2305
+ # {#answers_instance_for_subclass?}: only such a declaration answers a Ruby subclass with something other than
2306
+ # the `Nominal[subclass]` the dispatcher gives without one, so the dispatcher walks a subclass's ancestry, and
2307
+ # files that walk's dependency edges, only when this holds. Memoised per loader: one pass over `class_decls` for
2308
+ # the classes whose own non-core declaration writes `def self.new`, then their definitions.
2309
+ def singleton_new_departs_from_instance?
2310
+ return @state[:singleton_new_departs] if @state.key?(:singleton_new_departs)
2311
+
2312
+ @state[:singleton_new_departs] = scan_singleton_new_departs
2313
+ end
2314
+
2315
+ def scan_singleton_new_departs
2316
+ during_internal_demand do
2317
+ singleton_new_declarers.any? do |class_name|
2318
+ definition = singleton_method(class_name: class_name, method_name: :new)
2319
+ definition && honoured_singleton_new?(definition) && !answers_instance_for_subclass?(definition)
2320
+ end
2321
+ end
2322
+ rescue ::RBS::BaseError
2323
+ true
2324
+ end
2325
+ private :scan_singleton_new_departs
2326
+
2327
+ def singleton_new_declarers
2328
+ environment = env
2329
+ roots = core_stdlib_roots
2330
+ return [] if environment.nil? || roots.empty?
2331
+
2332
+ environment.class_decls.filter_map do |type_name, entry|
2333
+ declares = self.class.entry_declarations(entry).any? do |decl|
2334
+ name = self.class.declaration_buffer_name(decl)
2335
+ next false if name.nil? || roots.any? { |root| name.start_with?(root) }
2336
+
2337
+ decl.members.any? { |member| explicit_singleton_new_member?(member) }
2338
+ end
2339
+ type_name.to_s if declares
2340
+ end
2341
+ end
2342
+ private :singleton_new_declarers
2343
+
1944
2344
  # True when `class_name`'s RBS declaration is one the project wrote (see {#project_declared_classes}).
1945
2345
  def project_declared_class?(class_name)
1946
2346
  project_declared_classes.include?(class_name.to_s.delete_prefix("::"))
@@ -1977,7 +2377,7 @@ module Rigor
1977
2377
  next if type.is_a?(Type::Bot)
1978
2378
 
1979
2379
  path = self.class.declaration_buffer_name(entry.decl)
1980
- acc[name.to_sym] = [type, !path.nil? && CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }].freeze
2380
+ acc[name.to_sym] = [type, !path.nil? && core_stdlib_roots.any? { |root| path.start_with?(root) }].freeze
1981
2381
  end
1982
2382
  table.freeze
1983
2383
  rescue ::RBS::BaseError
@@ -2001,6 +2401,20 @@ module Rigor
2001
2401
  @state[:env_build_failure]
2002
2402
  end
2003
2403
 
2404
+ # Issue #2517 — builds the environment now when a core is selected, so a core it does not build from raises
2405
+ # its {CoreRootError} at the caller rather than at the first query inside analysis. No-op for the default core.
2406
+ def verify_core_root!
2407
+ env if @core_root
2408
+ nil
2409
+ end
2410
+
2411
+ # Issue #2883 — builds the environment now, for the default core as {#verify_core_root!} does for a selected one,
2412
+ # so no `type_of` builds it lazily (ADR-129 G1': the core-root Frame the build reads is never read inside typing).
2413
+ def prepare_for_typing!
2414
+ env
2415
+ nil
2416
+ end
2417
+
2004
2418
  # Issue #696 — the PER-CLASS sibling of {#env_build_failure}, one tier quieter in consequence: an
2005
2419
  # `RBS::DefinitionBuilder` failure ({#build_instance_definition} / {#build_singleton_definition}'s
2006
2420
  # rescue) leaves the class KNOWN but with no method surface, so every call on it — real methods and
@@ -2086,7 +2500,8 @@ module Rigor
2086
2500
  def member_consistency(proof: nil)
2087
2501
  key = proof ? :member_consistency_proven : :member_consistency
2088
2502
  @state[key] ||= self.class.member_consistency_for(
2089
- self.class.project_sig_files(@signature_paths), @virtual_rbs, proof: proof
2503
+ signature_files - self.class.project_sig_files(@deferred_signature_paths),
2504
+ @virtual_rbs, proof: proof
2090
2505
  ).records
2091
2506
  end
2092
2507
 
@@ -2214,6 +2629,77 @@ module Rigor
2214
2629
  end
2215
2630
  private :mixin_parent
2216
2631
 
2632
+ # Issue #2681 — every method name declared, on either side, by `name`'s RBS declarations and by the classes and
2633
+ # modules they inherit, include, prepend or extend, `::`-stripped names in `skip` excepted. Read from the
2634
+ # declarations as {#declared_ancestry} reads them, not through the definition builder: a definition that fails to
2635
+ # build reports a coverage diagnostic each time it is asked for. A superset of what the built definitions hold
2636
+ # is harmless to the one reader, a gate in front of ADR-43's override walk. Empty when the environment is
2637
+ # unavailable; the walk stops at 64 owners.
2638
+ def declared_method_names(name, skip: [])
2639
+ return Set.new if env.nil?
2640
+
2641
+ names = Set.new
2642
+ pending = [name.to_s.delete_prefix("::")]
2643
+ seen = Set.new
2644
+ until pending.empty? || seen.size > 64
2645
+ owner = pending.shift
2646
+ next if seen.include?(owner) || skip.include?(owner)
2647
+
2648
+ seen << owner
2649
+ entry = env.class_decls[parse_type_name(owner)]
2650
+ self.class.entry_declarations(entry).each { |decl| read_declared_methods(decl, names, pending) } if entry
2651
+ end
2652
+ names
2653
+ rescue ::RBS::BaseError
2654
+ Set.new
2655
+ end
2656
+
2657
+ def read_declared_methods(decl, names, pending)
2658
+ superclass = decl.respond_to?(:super_class) ? decl.super_class : nil
2659
+ pending << superclass.name.to_s.delete_prefix("::") if superclass
2660
+ return unless decl.respond_to?(:members)
2661
+
2662
+ decl.members.each do |member|
2663
+ case member
2664
+ when ::RBS::AST::Members::MethodDefinition, ::RBS::AST::Members::AttrReader
2665
+ names << member.name
2666
+ when ::RBS::AST::Members::AttrAccessor, ::RBS::AST::Members::AttrWriter
2667
+ names << member.name << :"#{member.name}="
2668
+ when ::RBS::AST::Members::Alias
2669
+ names << member.new_name
2670
+ when ::RBS::AST::Members::Include, ::RBS::AST::Members::Prepend, ::RBS::AST::Members::Extend
2671
+ pending << member.name.to_s.delete_prefix("::") unless member.name.interface?
2672
+ end
2673
+ end
2674
+ end
2675
+ private :read_declared_methods
2676
+
2677
+ # Issue #2321 — every class name an RBS class declaration names as its superclass, `::`-stripped: a class
2678
+ # absent from the set has no RBS-declared subclass. `nil` when the environment is unavailable or fails to read,
2679
+ # so a caller cannot mistake an unread environment for one without subclasses. Memoised per loader.
2680
+ def superclass_names
2681
+ return @state[:superclass_names] if @state.key?(:superclass_names)
2682
+
2683
+ @state[:superclass_names] = build_superclass_names&.freeze
2684
+ end
2685
+
2686
+ def build_superclass_names
2687
+ return nil if env.nil?
2688
+
2689
+ names = Set.new
2690
+
2691
+ env.class_decls.each_value do |entry|
2692
+ self.class.entry_declarations(entry).each do |decl|
2693
+ superclass = decl.respond_to?(:super_class) ? decl.super_class : nil
2694
+ names << superclass.name.to_s.delete_prefix("::") if superclass
2695
+ end
2696
+ end
2697
+ names
2698
+ rescue ::RBS::BaseError
2699
+ nil
2700
+ end
2701
+ private :build_superclass_names
2702
+
2217
2703
  # Yields every known class / module / alias name (top-level prefixed) currently loaded into the
2218
2704
  # environment. The cache producer that materialises the known-name set uses this so it never recurses
2219
2705
  # back through {#class_known?}.
@@ -2230,7 +2716,7 @@ module Rigor
2230
2716
  end
2231
2717
 
2232
2718
  # Yields every type-alias declaration loaded into the environment (`type foo = ...`).
2233
- # Yields `(RBS::TypeName, RBS::Environment::TypeAliasEntry)` pairs.
2719
+ # Yields `[type_name, decl_entry]` pairs.
2234
2720
  def each_type_alias_decl
2235
2721
  return enum_for(:each_type_alias_decl) unless block_given?
2236
2722
  return unless env
@@ -2288,7 +2774,7 @@ module Rigor
2288
2774
  # fail-soft: see #each_class_decl_annotation.
2289
2775
  end
2290
2776
 
2291
- # Returns a frozen `Hash<String, String>` mapping each loaded class / module name (top-level prefixed)
2777
+ # Returns a frozen mapping from each loaded class / module name (top-level prefixed)
2292
2778
  # to the file path of its FIRST declaration's RBS source. Used by {Rigor::Analysis::RunStats} to
2293
2779
  # attribute the type universe between "project sig/" (paths under the configured `signature_paths`)
2294
2780
  # and "bundled" (everything else — RBS core, stdlib libraries, gem-bundled RBS). Each value is a frozen
@@ -2408,6 +2894,34 @@ module Rigor
2408
2894
  nil
2409
2895
  end
2410
2896
 
2897
+ # #775 — the memo {Inference::RbsTypeTranslator} keeps, per loader, for the translated form of a closed alias
2898
+ # expansion (keyed by the expansion). It lives here because the translation reads this loader's environment
2899
+ # through any alias nested in the expansion ({#expand_type_alias}): another loader's `b` may differ under an
2900
+ # equal `a`, and the entry goes with the loader. The translator is its only reader and writer.
2901
+ def closed_alias_translations
2902
+ @state[:closed_alias_translations] ||= {}
2903
+ end
2904
+
2905
+ # An independent resolver for a read-only consumer. Its memo belongs to the caller, never the loader cache.
2906
+ def type_name_resolver
2907
+ environment = env
2908
+ return nil if environment.nil?
2909
+
2910
+ klass = ::RBS::Resolver::TypeNameResolver
2911
+ if klass.respond_to?(:build)
2912
+ klass.build(environment)
2913
+ else
2914
+ # Older RBS 3.x takes the environment instead of the current constructor's name tables.
2915
+ klass.new(environment) # rigor:disable call.wrong-arity
2916
+ end
2917
+ end
2918
+
2919
+ # The resolved alias declaration, without substituting type arguments or translating its AST.
2920
+ def type_alias_declaration(name)
2921
+ declarations = env&.type_alias_decls
2922
+ declarations&.[](name)&.decl
2923
+ end
2924
+
2411
2925
  # @return the resolved singleton (class object) definition for `class_name`. The
2412
2926
  # methods on this definition are the *class methods* of `class_name`, including those inherited from
2413
2927
  # `Class` and `Module` for class types. Returns nil for unknown names and on RBS build errors
@@ -2509,7 +3023,7 @@ module Rigor
2509
3023
  # singleton methods (e.g., `Array.new`) parameterize over the same `Elem` as instance methods.
2510
3024
  #
2511
3025
  # Returns an empty array for non-generic classes and for unknown names (the loader stays fail-soft).
2512
- # NOTE: in the `rbs` gem, `RBS::Definition#type_params` returns `Array<Symbol>` directly, not the AST
3026
+ # NOTE: in the `rbs` gem, `RBS::Definition#type_params` returns the names directly, not the AST
2513
3027
  # `TypeParam` object (those live on the AST level).
2514
3028
  #
2515
3029
  # When `cache_store` is set, the loader fetches the entire type-parameter-name table once (per
@@ -2539,6 +3053,31 @@ module Rigor
2539
3053
  NO_TYPE_PARAMS = [].freeze
2540
3054
  private_constant :NO_TYPE_PARAMS
2541
3055
 
3056
+ # Issue #2436 — the `RBS::AST::TypeParam` list a loaded declaration of `class_name` spells (`[unchecked out
3057
+ # E]` for `Array`), read off the declarations rather than through {#class_type_param_names}: a definition
3058
+ # build validates the entry and raises on one whose declarations already disagree, which is the env sig-gen
3059
+ # is asked to repair. The first declaration answers, as it is the one rbs compares every other against
3060
+ # ({.generic_arity_conflict}). Empty for a non-generic or unknown name, and when the environment did not
3061
+ # build.
3062
+ def class_declaration_type_params(class_name)
3063
+ memo = (@state[:declaration_type_params] ||= {})
3064
+ key = class_name.to_s.delete_prefix("::")
3065
+ memo.fetch(key) { memo[key] = read_declaration_type_params(key) }
3066
+ end
3067
+
3068
+ def read_declaration_type_params(key)
3069
+ rbs_name = parse_type_name(key)
3070
+ entry = rbs_name && env&.class_decls&.[](rbs_name)
3071
+ return NO_TYPE_PARAMS if entry.nil?
3072
+
3073
+ first = self.class.entry_declarations(entry).first
3074
+ params = first.respond_to?(:type_params) ? first.type_params : nil
3075
+ params.nil? || params.empty? ? NO_TYPE_PARAMS : params.dup.freeze
3076
+ rescue ::RBS::BaseError
3077
+ NO_TYPE_PARAMS
3078
+ end
3079
+ private :read_declaration_type_params
3080
+
2542
3081
  def class_ordering(lhs, rhs)
2543
3082
  @hierarchy.class_ordering(lhs, rhs)
2544
3083
  end
@@ -2811,7 +3350,7 @@ module Rigor
2811
3350
  producer.send(:compute, self)
2812
3351
  end
2813
3352
 
2814
- # ADR-15 Phase 2b — `Hash<String, Array<String>>` of normalised ancestor chains per class. Consumes
3353
+ # ADR-15 Phase 2b — the normalised ancestor chain of each class. Consumes
2815
3354
  # the existing `RbsClassAncestorTable` producer when `cache_store` is set; falls back to the
2816
3355
  # producer's `compute` otherwise. Used by {#reflection}.
2817
3356
  def ancestor_names_table
@@ -2846,6 +3385,7 @@ module Rigor
2846
3385
  # per AST node touched during analysis, multiplying per-file analysis cost by ~100x. Failures
2847
3386
  # short-circuit to `nil` here and are surfaced to the user via `warn_about_env_build_failure_once`.
2848
3387
  def env
3388
+ raise @state[:core_root_error] if @state[:core_root_error]
2849
3389
  return @state[:env] if @state[:env_loaded]
2850
3390
 
2851
3391
  @state[:env_loaded] = true
@@ -2854,11 +3394,24 @@ module Rigor
2854
3394
  warn_about_virtual_rbs_collisions
2855
3395
  @state[:env]
2856
3396
  rescue ::RBS::BaseError => e
3397
+ # Issue #2517 — an environment that does not build from an explicitly selected core is not analysed
3398
+ # against at all: the degraded `Dynamic[top]` env would report the very APIs the selection removes as
3399
+ # fine. Memoised like the nil env, so every later reader raises the same error instead of rebuilding.
3400
+ raise(@state[:core_root_error] = core_root_build_error(e)) if @core_root
3401
+
2857
3402
  record_env_build_failure(e)
2858
3403
  warn_about_env_build_failure_once(e)
2859
3404
  @state[:env] = nil
2860
3405
  end
2861
3406
 
3407
+ def core_root_build_error(error)
3408
+ CoreRootError.new(
3409
+ "core_root: #{@core_root.inspect} did not build an RBS environment " \
3410
+ "(#{error.class.name}: #{error.message.lines.first.to_s.strip})"
3411
+ )
3412
+ end
3413
+ private :core_root_build_error
3414
+
2862
3415
  # Tell the user, once per run, which of their `signature_paths:` `.rbs` files were skipped because they
2863
3416
  # do not parse ({RbsLoader.add_project_signatures} quarantines them so the rest of the env survives). A
2864
3417
  # skipped file's types are absent, so calls into them read `Dynamic[top]` — silently, without this. This
@@ -2966,7 +3519,7 @@ module Rigor
2966
3519
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
2967
3520
  end
2968
3521
 
2969
- # Full `Hash<String, RBS::Definition>` tables for the {#prewarm} / {#reflection} consumers (ADR-15
3522
+ # Full tables of `RBS::Definition` per class for the {#prewarm} / {#reflection} consumers (ADR-15
2970
3523
  # Phase 2b/4b.x), which need every definition materialised up front. Built from the (cached) env via
2971
3524
  # `RBS::DefinitionBuilder` — ADR-54 WD1 retired the disk blobs these used to `Marshal.load` (building
2972
3525
  # all definitions from a cached env is faster), so the eager-table cost is now a compute, not a load.
@@ -3007,7 +3560,11 @@ module Rigor
3007
3560
  libraries: @libraries,
3008
3561
  signature_paths: @signature_paths,
3009
3562
  virtual_rbs: @virtual_rbs,
3010
- deferred_signature_paths: @deferred_signature_paths
3563
+ deferred_signature_paths: @deferred_signature_paths,
3564
+ project_signature_paths: @project_signature_paths,
3565
+ core_root: @core_root,
3566
+ subordinate_signature_paths: @subordinate_signature_paths,
3567
+ collection_signature_paths: @collection_signature_paths
3011
3568
  )
3012
3569
  end
3013
3570
 
@@ -3034,6 +3591,15 @@ module Rigor
3034
3591
  rbs_name = canonical_module_name(rbs_name)
3035
3592
  return nil unless env.class_decls.key?(rbs_name)
3036
3593
 
3594
+ # rbs's `AncestorBuilder#singleton_ancestors` has no cycle check: a class whose superclass chain loops
3595
+ # (`class S < ::S`, which a collection copy of the same class used to hide, #2441) recurses until the stack
3596
+ # runs out and aborts the run, where `#instance_ancestors` raises `RBS::RecursiveAncestorError`. Walking the
3597
+ # instance side first turns the loop into that error, so it is this class's build failure. Memoised by the
3598
+ # builder, so a class whose instance side was already built pays nothing. Any other stack exhaustion stays
3599
+ # fatal: it depends on how deep the analysis already was, and must not be memoised as a build failure.
3600
+ if env.class_decls[rbs_name].is_a?(::RBS::Environment::ClassEntry)
3601
+ builder.ancestor_builder.instance_ancestors(rbs_name)
3602
+ end
3037
3603
  builder.build_singleton(rbs_name)
3038
3604
  rescue ::RBS::BaseError => e
3039
3605
  store_definition_build_detail(class_name, e)
@@ -3063,7 +3629,8 @@ module Rigor
3063
3629
  key = class_name.to_s.delete_prefix("::")
3064
3630
  details = ((@state[:definition_build_details] ||= {})[key] ||= [])
3065
3631
  details << [key, error.class.name.to_s, definition_build_member(error),
3066
- definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
3632
+ definition_build_conflict_buffers(error, key), unresolved_type_name_detail(error),
3633
+ variable_mixin_count(error, key)].freeze
3067
3634
  end
3068
3635
 
3069
3636
  # Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
@@ -3214,7 +3781,7 @@ module Rigor
3214
3781
  end
3215
3782
 
3216
3783
  # The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
3217
- # once per class per PROCESS, a `RBS::DefinitionBuilder` failure caught in
3784
+ # once per class per loader, a `RBS::DefinitionBuilder` failure caught in
3218
3785
  # {#build_instance_definition} / {#build_singleton_definition}'s rescue. Without it, `class_known?`
3219
3786
  # stays true (it only consults {#known_class_names_set}, never a definition build), so every call on
3220
3787
  # the class — real methods and typos alike — silently degrades to `Dynamic[top]`.
@@ -3251,14 +3818,14 @@ module Rigor
3251
3818
 
3252
3819
  def definition_build_failure_warning(class_name, error)
3253
3820
  first_line = error.message.to_s.lines.first.to_s.strip
3254
- buffers = definition_build_conflict_buffers(error)
3821
+ buffers = definition_build_conflict_buffers(error, class_name)
3255
3822
  collisions =
3256
3823
  if buffers.empty?
3257
3824
  ""
3258
3825
  else
3259
3826
  listed = buffers.first(QUARANTINE_WARN_LIMIT)
3260
3827
  more = buffers.size - listed.size
3261
- lines = listed.map { |name| " - #{name}" }
3828
+ lines = listed.map { |name| " - #{signature_source_label(name)}" }
3262
3829
  lines << " … and #{more} more" if more.positive?
3263
3830
  "\n Colliding declaration(s):\n#{lines.join("\n")}"
3264
3831
  end
@@ -3267,6 +3834,14 @@ module Rigor
3267
3834
  "`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
3268
3835
  end
3269
3836
 
3837
+ # Issue #2440 — a `virtual:<plugin>:<path>` buffer named by the `.rb` file it was synthesized from.
3838
+ def signature_source_label(name)
3839
+ prefix, plugin_id, path = name.to_s.split(":", 3)
3840
+ return name.to_s unless prefix == "virtual" && plugin_id && path && !path.empty?
3841
+
3842
+ "#{path} (inline RBS via #{plugin_id})"
3843
+ end
3844
+
3270
3845
  def warn_about_deferred_definition_build_failures
3271
3846
  return if @state[:definition_build_summary_warned]
3272
3847
 
@@ -3305,10 +3880,11 @@ module Rigor
3305
3880
  # `InvalidOverloadMethodError` carry `#members` (each with a `#location`);
3306
3881
  # `DuplicatedInterfaceMethodDefinitionError` carries a single `#member`; most others
3307
3882
  # (`NoSuperclassFoundError`, `UnknownMethodAliasError`, `RecursiveAncestorError`, …) carry only a bare
3308
- # `#location`. Falls back to an empty list — the warning still names the class and the exception's own
3309
- # message — for the remainder (e.g. `SuperclassMismatchError`, which carries neither).
3310
- def definition_build_conflict_buffers(error)
3311
- definition_build_error_locations(error)
3883
+ # `#location`. A duplicated instance variable carries one location and a `SuperclassMismatchError` none,
3884
+ # so both read the class entry's declarations for the other side (#2440). Falls back to an empty list —
3885
+ # the warning still names the class and the exception's own message — for the remainder.
3886
+ def definition_build_conflict_buffers(error, class_name = nil)
3887
+ definition_build_error_locations(error, class_name)
3312
3888
  .compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s)
3313
3889
  .reject { |name| name == CACHED_LOCATION_BUFFER_NAME }.uniq.freeze
3314
3890
  rescue ::RBS::BaseError, StandardError
@@ -3317,14 +3893,100 @@ module Rigor
3317
3893
 
3318
3894
  # The `RBS::Location`s an error carries, by whichever accessor its class happens to expose (see the
3319
3895
  # shape survey above). Split out so the buffer-name extraction reads as one pipeline.
3320
- def definition_build_error_locations(error)
3896
+ def definition_build_error_locations(error, class_name = nil)
3321
3897
  return Array(error.members).map(&:location) if error.respond_to?(:members) && error.members
3322
3898
  return [error.member.location] if error.respond_to?(:member) && error.member
3899
+ # `VariableDuplicationError` is younger than the rbs floor the gemspec allows.
3900
+ if defined?(::RBS::VariableDuplicationError) && error.is_a?(::RBS::VariableDuplicationError)
3901
+ return variable_duplication_locations(error, class_name)
3902
+ end
3903
+ return superclass_mismatch_locations(error) if error.is_a?(::RBS::SuperclassMismatchError)
3323
3904
  return [error.location] if error.respond_to?(:location)
3324
3905
 
3325
3906
  []
3326
3907
  end
3327
3908
 
3909
+ # Issue #2440 — a duplicated `@x` / `self.@x` carries the location of ONE of its two declarations, so
3910
+ # the file list named only that side (typically the `virtual:rbs-inline:` buffer, leaving out the
3911
+ # committed `.rbs` the user can actually edit). The class entry's declarations name the rest: every
3912
+ # declaration of the class that declares the same variable, of the same kind. When the variable's
3913
+ # owner is a module the failing class mixes in (rbs counts it once per declaration that does), the
3914
+ # declarations of the failing class that mix that module in are named too: they are the duplicate.
3915
+ def variable_duplication_locations(error, class_name = nil)
3916
+ [error.location, *variable_declarations(error).map(&:location), *mixin_locations(class_name, error.type_name)]
3917
+ end
3918
+
3919
+ # Every declaration of the variable the error names, of the error's kind, in its owner's entry.
3920
+ def variable_declarations(error)
3921
+ member_class =
3922
+ if error.is_a?(::RBS::ClassInstanceVariableDuplicationError)
3923
+ ::RBS::AST::Members::ClassInstanceVariable
3924
+ else
3925
+ ::RBS::AST::Members::InstanceVariable
3926
+ end
3927
+ class_entry_decls(error.type_name).flat_map do |decl|
3928
+ decl_members(decl).select { |m| m.instance_of?(member_class) && m.name == error.variable_name }
3929
+ end
3930
+ end
3931
+
3932
+ # Issue #2440 — how many times `class_name`'s own declarations mix in the module that owns the
3933
+ # duplicated variable, when that is what duplicated it: the owner is another module, it declares the
3934
+ # variable exactly once, and the class mixes it in at least twice (in one declaration or across
3935
+ # several). nil otherwise — a variable duplicated in the owner itself and inherited (`Sub < Base`)
3936
+ # is not a double mixin. Carried in the failure tuple's sixth slot so the advice can tell the two
3937
+ # apart on a replayed tuple too.
3938
+ def variable_mixin_count(error, class_name)
3939
+ return nil unless defined?(::RBS::VariableDuplicationError) && error.is_a?(::RBS::VariableDuplicationError)
3940
+
3941
+ owner = parse_type_name(class_name)
3942
+ return nil if owner.nil? || owner == error.type_name
3943
+ return nil unless variable_declarations(error).size == 1
3944
+
3945
+ count = class_entry_decls(owner).sum do |decl|
3946
+ decl_members(decl).count { |m| mixin_of?(m, error.type_name) }
3947
+ end
3948
+ count >= 2 ? count : nil
3949
+ rescue ::RBS::BaseError, StandardError
3950
+ nil
3951
+ end
3952
+
3953
+ MIXIN_MEMBERS = [::RBS::AST::Members::Include, ::RBS::AST::Members::Extend,
3954
+ ::RBS::AST::Members::Prepend].freeze
3955
+ private_constant :MIXIN_MEMBERS
3956
+
3957
+ # `resolve_type_names` has made every mixin name absolute.
3958
+ def mixin_of?(member, module_name)
3959
+ MIXIN_MEMBERS.any? { |klass| member.is_a?(klass) } && member.name == module_name
3960
+ end
3961
+
3962
+ def mixin_locations(class_name, module_name)
3963
+ return [] if class_name.nil?
3964
+
3965
+ owner = parse_type_name(class_name)
3966
+ return [] if owner.nil? || owner == module_name
3967
+
3968
+ class_entry_decls(owner).filter_map do |decl|
3969
+ decl.location if decl_members(decl).any? { |m| mixin_of?(m, module_name) }
3970
+ end
3971
+ end
3972
+
3973
+ def decl_members(decl)
3974
+ decl.respond_to?(:members) ? Array(decl.members) : []
3975
+ end
3976
+
3977
+ # Issue #2440 — `SuperclassMismatchError` carries the class entry and no location of its own: the
3978
+ # colliding declarations are the ones that name a superclass. {.entry_declarations} reads the entry on
3979
+ # both rbs majors (`each_decl` on 4.x, `MultiEntry::D` wrappers on 3.x).
3980
+ def superclass_mismatch_locations(error)
3981
+ decls = self.class.entry_declarations(error.entry)
3982
+ decls.select { |decl| decl.respond_to?(:super_class) && decl.super_class }.map(&:location)
3983
+ end
3984
+
3985
+ def class_entry_decls(type_name)
3986
+ entry = env&.class_decls&.[](type_name)
3987
+ entry.nil? ? [] : self.class.entry_declarations(entry)
3988
+ end
3989
+
3328
3990
  # The member the failure is ABOUT, taken off the error object rather than parsed out of its message.
3329
3991
  #
3330
3992
  # Issue #696 — the message cannot serve here. It is built from `RBS::Location`s, and the ADR-54 env