rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -10,6 +10,10 @@ 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"
15
+ require_relative "ruby_library_roots"
16
+ require_relative "core_root"
13
17
  require_relative "../rooted_glob"
14
18
 
15
19
  module Rigor
@@ -95,8 +99,33 @@ module Rigor
95
99
  # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the
96
100
  # per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
97
101
  # intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.
98
- def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [])
99
- inputs = [signature_paths, virtual_rbs, deferred_signature_paths]
102
+ #
103
+ # @param project_signature_paths — issue #2422 — the project's own `signature_paths:` (not the collection,
104
+ # gem or plugin directories also on `signature_paths`): a plugin member another source declares composes
105
+ # with it as an overload unless that source is the project's own ({DeferredSignaturePlan}).
106
+ # @param core_root — issue #2517 — a validated {CoreRoot} directory loaded in place of the `rbs` gem's
107
+ # `core/`, or nil for the default core.
108
+ def build_env_for(libraries:, signature_paths:, virtual_rbs: [], deferred_signature_paths: [],
109
+ project_signature_paths: [], core_root: nil)
110
+ inputs = [signature_paths, virtual_rbs, deferred_signature_paths, project_signature_paths]
111
+ with_core_root(core_root) { build_env_from(libraries, inputs) }
112
+ end
113
+
114
+ # Issue #2517 — the core root {.library_loader} builds every loader of one environment build against. Held
115
+ # for the build rather than threaded through each attempt, because every trial and fallback of
116
+ # {.build_env_from} must read the same core, and a parameter missed on one of them would build that attempt
117
+ # against the default core without a sign. Thread-local, so a concurrent build on another thread keeps its
118
+ # own.
119
+ def with_core_root(core_root)
120
+ previous = Thread.current[:rigor_rbs_core_root]
121
+ Thread.current[:rigor_rbs_core_root] = core_root
122
+ yield
123
+ ensure
124
+ Thread.current[:rigor_rbs_core_root] = previous
125
+ end
126
+
127
+ def build_env_from(libraries, inputs)
128
+ signature_paths, virtual_rbs = inputs
100
129
  env, gated_dirs = whole_gated_attempt(libraries, inputs)
101
130
  return env if gated_dirs.empty?
102
131
 
@@ -170,7 +199,27 @@ module Rigor
170
199
  #
171
200
  # @param gated_plan — issue #1713 — a {GatedSignaturePlan} loaded in place of the gated directories: its
172
201
  # kept declarations where the directories would have loaded, its shells after the project's signatures.
173
- def build_env_attempt(libraries, signature_paths, virtual_rbs, deferred_signature_paths, gated_plan: nil)
202
+ #
203
+ # Issue #2470 — re-made without each {VendoredSignaturePlan} taker the attempt quarantined
204
+ # ({.quarantined_takers}), so a vendored member stands down only to a file the env keeps. Each round adds
205
+ # at least one file to the excluded set, and a round that adds none returns, so the loop ends.
206
+ def build_env_attempt(libraries, *inputs, gated_plan: nil)
207
+ excluded = Set.new
208
+ loop do
209
+ env, gated_dirs, plan = planned_env_attempt(libraries, *inputs, gated_plan: gated_plan,
210
+ vendored_excluded: excluded)
211
+ dropped = quarantined_takers(env, plan)
212
+ return [env, gated_dirs] if dropped.empty? || dropped.subset?(excluded)
213
+
214
+ excluded |= dropped
215
+ end
216
+ end
217
+
218
+ # One {.build_env_attempt} round, which also returns its {VendoredSignaturePlan}.
219
+ #
220
+ # @param vendored_excluded — signature files the vendored plan is made without.
221
+ def planned_env_attempt(libraries, signature_paths, virtual_rbs, deferred_signature_paths,
222
+ project_signature_paths = [], gated_plan: nil, vendored_excluded: Set.new)
174
223
  libraries = libraries_without_shadowed_bigdecimal_math(libraries)
175
224
  rbs_loader, loaded_libraries = library_loader(libraries)
176
225
  gated_dirs = loadable_gated_dirs(libraries, loaded_libraries)
@@ -180,13 +229,16 @@ module Rigor
180
229
  # type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06
181
230
  # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay
182
231
  # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path.
183
- add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s), gated_plan ? [] : gated_dirs)
184
- env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
185
- gated_plan&.leading&.each { |source| add_parsed_decls(env, *source) }
186
232
  project_files = project_sig_files(signature_paths)
233
+ undeferred_files = project_files - project_sig_files(deferred_signature_paths)
234
+ env, vendored_plan = bundled_env(rbs_loader, loaded_libraries, gated_plan ? [] : gated_dirs,
235
+ undeferred_files, vendored_excluded)
236
+ gated_plan&.leading&.each { |source| add_parsed_decls(env, *source) }
187
237
  # Issue #1075 — decided from the inputs before either side enters `env`: the consistency rule may
188
238
  # stand the `.rbs` member down, and that has to happen before its file is added.
189
- consistency = member_consistency_for(project_files, virtual_rbs)
239
+ # Issue #2422 — a bundled plugin's deferred files are not the project's: they never take part in the
240
+ # inline-vs-`.rbs` consistency rule, and stand down to an inline member like to any other source.
241
+ consistency = member_consistency_for(undeferred_files, virtual_rbs)
190
242
  add_project_signatures(env, signature_paths, deferred_signature_paths,
191
243
  standdowns: consistency.signature_standdowns)
192
244
  gated_plan&.shells&.each { |source| add_parsed_decls(env, *source) }
@@ -200,16 +252,52 @@ module Rigor
200
252
  # Issues #824 / #1075 — every member a `.rbs` file also declares was ranked above; the inline side of
201
253
  # each pair the `.rbs` won stands down here ({.add_virtual_rbs}), so the two never collide.
202
254
  add_virtual_rbs(env, virtual_rbs, inline_standdowns: consistency.inline_standdowns)
255
+ # Issues #610 / #2422 — the plugins' files last, so each is planned against every other source.
256
+ add_plugin_signatures(env, deferred_signature_paths, project_signature_paths, virtual_rbs)
203
257
  synthesize_missing_namespaces(env)
204
258
  # Issue #777 — resolve-time backstop: also unload colliding PROJECT buffers (class-vs-module /
205
259
  # constant redeclarations against bundled RBS). Virtual culprits are preferred when both appear.
206
260
  env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs, project_files: project_files)
207
- [stub_missing_referenced_types(env, resolved, project_files), gated_dirs]
261
+ [stub_missing_referenced_types(env, resolved, project_files), gated_dirs, vendored_plan]
262
+ end
263
+
264
+ # Issue #2470 — the {VendoredSignaturePlan} takers `env` does not hold: a file the build quarantined after a
265
+ # vendored member stood down to it (a `DuplicatedDeclarationError`, a class-vs-module clash). The answer reads
266
+ # only the build's inputs, so a cold, a warm and a `--no-cache` run agree.
267
+ def quarantined_takers(env, plan)
268
+ return Set.new if plan.takers.empty? || env.nil?
269
+
270
+ buffers = loaded_signature_buffers(env)
271
+ return Set.new if buffers.nil?
272
+
273
+ present = buffers.to_set { |buffer| buffer.name.to_s }
274
+ plan.takers.reject { |file| present.include?(file) }.to_set
275
+ end
276
+
277
+ # The environment of the loader's libraries and the Rigor-shipped signatures. Issue #2470 — a vendored
278
+ # directory with a member a `signature_files` file also declares loads from its {VendoredSignaturePlan},
279
+ # right after the batch, instead of whole on it.
280
+ def bundled_env(rbs_loader, loaded_libraries, gated_dirs, signature_files, excluded)
281
+ loaded_library_names = loaded_libraries.to_set(&:to_s)
282
+ plan = vendored_signature_plan(loaded_library_names, signature_files, excluded)
283
+ add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs, planned: plan.dirs)
284
+ env = unload_upstream_core_shims(RBS::Environment.from_loader(rbs_loader), rbs_loader)
285
+ plan.sources.each { |source| add_parsed_decls(env, *source) }
286
+ [env, plan]
287
+ end
288
+
289
+ # Issue #2422 — {.add_deferred_signatures} over the plugins' files, with the buffer names of the project's
290
+ # own signature sources, which a plugin member stands down to rather than composing with: its
291
+ # `signature_paths:` files and its inline RBS.
292
+ def add_plugin_signatures(env, deferred_signature_paths, project_signature_paths, virtual_rbs)
293
+ own = project_sig_files(project_signature_paths) | virtual_rbs.to_set { |name, _| name.to_s }
294
+ add_deferred_signatures(env, project_sig_files(deferred_signature_paths), own)
208
295
  end
209
296
 
210
297
  # An `RBS::EnvironmentLoader` with every library of `libraries` that resolves added, and those libraries.
211
298
  def library_loader(libraries)
212
- rbs_loader = RBS::EnvironmentLoader.new
299
+ core_root = Thread.current[:rigor_rbs_core_root]
300
+ rbs_loader = core_root ? RBS::EnvironmentLoader.new(core_root: Pathname(core_root)) : RBS::EnvironmentLoader.new
213
301
  # Issue #1700 — a `RequiredFeatures` token names a gated vendored directory, never an RBS library.
214
302
  loaded_libraries = libraries.select do |library|
215
303
  next false if RequiredFeatures.feature_of_token(library)
@@ -578,7 +666,6 @@ module Rigor
578
666
  strip_members!(decls, standdowns[file]) if standdowns.key?(file)
579
667
  add_project_parsed_decls(env, buffer, directives, decls)
580
668
  end
581
- add_deferred_signatures(env, deferred, standdowns)
582
669
  end
583
670
 
584
671
  # Issue #777 — add one project signature transactionally. `RBS::Environment#add_source` appends to
@@ -616,7 +703,15 @@ module Rigor
616
703
  # (rigor-activerecord's is `relation.rbs`, declaring exactly `Relation`), so dropping the file drops
617
704
  # exactly the conflicting declaration. Splitting a plugin file that mixes a colliding class with
618
705
  # innocent ones is the change to make if that ever stops being true.
619
- def add_deferred_signatures(env, deferred_files, standdowns = {})
706
+ #
707
+ # Issue #2422 — at equal arity the plugin reopens the class, and a method both sources declare fails the
708
+ # definition build (`DuplicatedMethodDefinitionError`) for the class and everything under it; a constant
709
+ # both declare raises `DuplicatedDeclarationError` and drops the plugin's whole file. So the file is planned
710
+ # against the env before it is added ({DeferredSignaturePlan}): a colliding method composes with another
711
+ # source's `def` as an overload continuation, or stands down when the other source is the project's own
712
+ # (`own`, buffer names) or cannot be continued; a colliding declaration stands down on its own; the rest
713
+ # of the file loads. {.deferred_member_standdowns} reads what stood down back off the built env.
714
+ def add_deferred_signatures(env, deferred_files, own = Set.new)
620
715
  deferred_files.sort.each do |file|
621
716
  parsed = parse_signature_file(file)
622
717
  next if parsed.nil?
@@ -624,8 +719,7 @@ module Rigor
624
719
  buffer, directives, decls = parsed
625
720
  next if generic_arity_conflict(env, decls)
626
721
 
627
- strip_members!(decls, standdowns[file]) if standdowns.key?(file)
628
- add_project_parsed_decls(env, buffer, directives, decls)
722
+ add_project_parsed_decls(env, buffer, directives, DeferredSignaturePlan.plan(decls, env, own))
629
723
  end
630
724
  end
631
725
 
@@ -647,6 +741,24 @@ module Rigor
647
741
  end
648
742
  end
649
743
 
744
+ # Issue #2422 — the members {.add_deferred_signatures} left out of each deferred file it loaded, as
745
+ # `[absolute_path, declaration, nil, nil, cause_file, :member]` — `declaration` reading `Object#acts_like?`
746
+ # or `Time.at`, `cause_file` the buffer of the source that declares it, or nil when that cannot be named.
747
+ # Derived from the built env for {.deferred_standdowns}' reason; a file that stood down whole is
748
+ # {.deferred_standdowns}' row, and one the env does not hold has nothing to read back.
749
+ def deferred_member_standdowns(env, deferred_paths)
750
+ return [] if env.nil? || deferred_paths.nil? || deferred_paths.empty?
751
+
752
+ loaded = loaded_signature_buffers(env)&.to_set { |buffer| buffer.name.to_s } || Set.new
753
+ project_sig_files(deferred_paths).sort.flat_map do |file|
754
+ parsed = loaded.include?(file) ? parse_signature_file(file) : nil
755
+ next [] if parsed.nil? || generic_arity_conflict(env, parsed[2])
756
+
757
+ rows = DeferredSignaturePlan.standdown_rows(env, file, parsed[2]).sort_by(&:first)
758
+ rows.map { |label, cause| [file, label, nil, nil, cause, :member] }
759
+ end
760
+ end
761
+
650
762
  # The first `[class_name, existing_arity, incoming_arity, existing_file]` where `decls` would
651
763
  # re-declare a class the env already has with a DIFFERENT number of type parameters, or nil when
652
764
  # they agree everywhere. Equal arity is not a conflict: RBS reopens the class, which is the ordinary
@@ -689,9 +801,9 @@ module Rigor
689
801
  decls.each do |decl|
690
802
  case decl
691
803
  when ::RBS::AST::Declarations::Module
692
- each_declared_class(decl.members, prefix + [decl.name.to_s], &block)
804
+ each_declared_class(decl.members, declared_class_path(decl, prefix), &block)
693
805
  when ::RBS::AST::Declarations::Class
694
- inner = prefix + [decl.name.to_s]
806
+ inner = declared_class_path(decl, prefix)
695
807
  block.call("::#{inner.join('::')}", decl.type_params.size)
696
808
  each_declared_class(decl.members, inner, &block)
697
809
  end
@@ -1132,11 +1244,12 @@ module Rigor
1132
1244
  end
1133
1245
 
1134
1246
  # `[owners, shadowable]`. `owners` is `{[class_name, method_name, kind] => [signature_path, member,
1135
- # visibility]}` for every method-shaped member the project's own `signature_paths:` files declare and
1136
- # the inline sources also name; the first file in sorted order owns a key two `.rbs` files declare
1137
- # (that pair fails the definition build anyway). `shadowable` is every `::` segment of every class /
1138
- # module name the inline sources and the signature files declare — the names a relative reference
1139
- # might resolve to inside an enclosing namespace ({MemberConsistency::Comparator#faithful?}).
1247
+ # visibility]}` for every method-shaped or instance-variable member the project's own
1248
+ # `signature_paths:` files declare and the inline sources also name; the first file in sorted order
1249
+ # owns a key two `.rbs` files declare (that pair fails the definition build anyway). `shadowable` is
1250
+ # every `::` segment of every class / module name the inline sources and the signature files declare
1251
+ # — the names a relative reference might resolve to inside an enclosing namespace
1252
+ # ({MemberConsistency::Comparator#faithful?}).
1140
1253
  #
1141
1254
  # The parse is scoped by a substring pre-filter over each file's text: a declaration has to spell its
1142
1255
  # own name, so a signature file mentioning none of the names the inline sources declare cannot
@@ -1162,14 +1275,14 @@ module Rigor
1162
1275
  def index_signature_members(index, file, decls)
1163
1276
  return if decls.nil?
1164
1277
 
1165
- each_declared_member(decls) do |class_name, member, visibility|
1166
- member_method_keys(member).each do |method_name, kind|
1278
+ each_declared_member(decls) do |class_name, member, visibility, nesting|
1279
+ member_keys(member, nesting).each do |method_name, kind|
1167
1280
  index[[class_name, method_name, kind]] ||= [file, member, visibility].freeze
1168
1281
  end
1169
1282
  end
1170
1283
  end
1171
1284
 
1172
- # The directive heads a refinement {MemberConsistency.refinement_conflicts} checks, used to skip
1285
+ # The directive heads a refinement {MemberConsistency::Comparator#refinement_conflicts} checks, used to skip
1173
1286
  # parsing an inline source that carries none when no `.rbs` overlaps it. Narrower than `rigor:v1:` on
1174
1287
  # purpose: rbs-inline's output marks every skeleton `%a{rigor:v1:inferred-return}`, so that prefix is
1175
1288
  # in nearly every synthesized source.
@@ -1189,49 +1302,65 @@ module Rigor
1189
1302
  decls = parse_signature_source(name, content)
1190
1303
  next if decls.nil?
1191
1304
 
1192
- each_declared_member(decls) do |class_name, member, visibility|
1193
- entries << [name.to_s, class_name, member, visibility]
1305
+ each_declared_member(decls) do |class_name, member, visibility, nesting|
1306
+ entries << [name.to_s, class_name, member, visibility, nesting]
1194
1307
  end
1195
1308
  end
1196
1309
  end
1197
1310
 
1198
- # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`).
1199
- # Nested declarations are descended into after the rejection, and a declaration member contributes no
1200
- # method key, so nesting is never itself removed.
1201
- def strip_members!(decls, keys, prefix = [])
1311
+ # Removes, in place, every member contributing a key in `keys` (a Set of `[class, method, kind]`,
1312
+ # instance-variable keys included, #2440). Nested declarations are descended into after the
1313
+ # rejection, and a declaration member contributes no key, so nesting is never itself removed.
1314
+ def strip_members!(decls, keys, prefix = [], written = [])
1202
1315
  Array(decls).each do |decl|
1203
1316
  next unless declaration_with_members?(decl)
1204
1317
 
1205
- inner = prefix + [decl.name.to_s.delete_prefix("::")]
1318
+ inner = declared_class_path(decl, prefix)
1319
+ nesting = written + [decl.name.to_s]
1206
1320
  class_name = inner.join("::")
1207
1321
  decl.members.reject! do |member|
1208
- member_method_keys(member).any? { |method_name, kind| keys.include?([class_name, method_name, kind]) }
1322
+ member_keys(member, nesting.join("/")).any? do |method_name, kind|
1323
+ keys.include?([class_name, method_name, kind])
1324
+ end
1209
1325
  end
1210
- strip_members!(decl.members, keys, inner)
1326
+ strip_members!(decl.members, keys, inner, nesting)
1211
1327
  end
1212
1328
  end
1213
1329
 
1214
- # Yields `[class_name, member, visibility]` for every member of every class / module declaration in
1215
- # `decls`, descending through nesting. The enclosing path is accumulated and the name rendered
1216
- # `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same — which is
1217
- # what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the member's
1218
- # own (`private def x`) or else the one the enclosing `private` / `public` section sets; a singleton
1219
- # method takes no section, as in Ruby.
1220
- def each_declared_member(decls, prefix = [], &block)
1330
+ # The class path a declaration contributes to member keys: its name `::`-stripped, under the enclosing
1331
+ # path unless the name is absolute. A leading `::` resets the path, as RBS resolves `module A; class
1332
+ # ::Abs` to the top-level `Abs` (issue #2474), and so do the children of that declaration. The
1333
+ # `nesting` a mixin is keyed by is not reset: it keeps the names as written, which is the lexical
1334
+ # scope a relative mixin name resolves through.
1335
+ def declared_class_path(decl, prefix)
1336
+ base = decl.name.to_s.start_with?("::") ? [] : prefix
1337
+ base + [decl.name.to_s.delete_prefix("::")]
1338
+ end
1339
+
1340
+ # Yields `[class_name, member, visibility, nesting]` for every member of every class / module
1341
+ # declaration in `decls`, descending through nesting. The enclosing path is accumulated and the name
1342
+ # rendered `::`-stripped, so a `module Foo; class Bar` and a `class Foo::Bar` elsewhere key the same —
1343
+ # which is what makes a `sig/` declaration and an inline one comparable at all. `visibility` is the
1344
+ # member's own (`private def x`) or else the one the enclosing `private` / `public` section sets; a
1345
+ # singleton method takes no section, as in Ruby. `nesting` is the declaration names exactly as
1346
+ # written, outermost first, joined by `/` (`Foo/Bar` against `Foo::Bar`): a mixin's relative name
1347
+ # resolves through it, so {MemberConsistency.member_keys} keys a mixin by it (#2440).
1348
+ def each_declared_member(decls, prefix = [], written = [], &block)
1221
1349
  Array(decls).each do |decl|
1222
1350
  next unless declaration_with_members?(decl)
1223
1351
 
1224
- inner = prefix + [decl.name.to_s.delete_prefix("::")]
1352
+ inner = declared_class_path(decl, prefix)
1353
+ nesting = written + [decl.name.to_s]
1225
1354
  class_name = inner.join("::")
1226
1355
  section = :public
1227
1356
  decl.members.each do |member|
1228
1357
  case member
1229
1358
  when ::RBS::AST::Members::Private then section = :private
1230
1359
  when ::RBS::AST::Members::Public then section = :public
1231
- else block.call(class_name, member, member_visibility(member, section))
1360
+ else block.call(class_name, member, member_visibility(member, section), nesting.join("/"))
1232
1361
  end
1233
1362
  end
1234
- each_declared_member(decl.members, inner, &block)
1363
+ each_declared_member(decl.members, inner, nesting, &block)
1235
1364
  end
1236
1365
  end
1237
1366
 
@@ -1253,6 +1382,12 @@ module Rigor
1253
1382
  MemberConsistency.member_method_keys(member)
1254
1383
  end
1255
1384
 
1385
+ # {MemberConsistency.member_keys} — the method keys plus an instance variable's and a mixin's (#2440),
1386
+ # which is what the inline-vs-`sig/` consistency rule indexes and strips by.
1387
+ def member_keys(member, nesting = nil)
1388
+ MemberConsistency.member_keys(member, nesting)
1389
+ end
1390
+
1256
1391
  # A class / module declaration header, as the pre-filter in {.signature_member_index} reads it out of
1257
1392
  # synthesized RBS text.
1258
1393
  INLINE_DECLARATION_NAME = /^[ \t]*(?:class|module)[ \t]+([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)/
@@ -1427,8 +1562,12 @@ module Rigor
1427
1562
  #
1428
1563
  # @param loaded_library_names — libraries that actually resolved on this loader.
1429
1564
  # @param gated_dirs — the {RequiredFeatures}-gated directory basenames this library list activates.
1430
- def add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs = [])
1565
+ #
1566
+ # @param planned — issue #2470 — the {VendoredSignaturePlan} directories, which load from the plan instead.
1567
+ def add_bundled_signatures(rbs_loader, loaded_library_names, gated_dirs = [], planned: [])
1431
1568
  vendored_gem_sig_paths.each do |path|
1569
+ next if planned.include?(path)
1570
+
1432
1571
  rbs_loader.add(path: path) if vendored_dir_loads?(path, loaded_library_names, gated_dirs)
1433
1572
  end
1434
1573
  core_overlay_sig_paths.each do |dir|
@@ -1444,6 +1583,18 @@ module Rigor
1444
1583
  end
1445
1584
  end
1446
1585
 
1586
+ # Issue #2470 — the ungated vendored directories this build loads, planned against the signature files
1587
+ # that outrank them ({VendoredSignaturePlan}). The gated ones have their own plan (#1713).
1588
+ #
1589
+ # @param signature_files — the `signature_paths:` files a bundled plugin did not contribute: the project's
1590
+ # own, a collection's and a gem's. A plugin's files are planned against the env instead (#2422).
1591
+ def vendored_signature_plan(loaded_library_names, signature_files, excluded = Set.new)
1592
+ roots = vendored_gem_sig_paths.select do |path|
1593
+ !RequiredFeatures.gated_dir?(path.basename.to_s) && vendored_dir_loads?(path, loaded_library_names, [])
1594
+ end
1595
+ VendoredSignaturePlan.build(roots, signature_files, excluded)
1596
+ end
1597
+
1447
1598
  # Issue #1700 — the {RequiredFeatures}-gated directories `libraries` activates, less one whose feature
1448
1599
  # resolved as an RBS library here (`libraries: [prime]` on a host whose `prime` gem ships its `sig/`),
1449
1600
  # whose own copy is then already loaded. {.build_env_for} then keeps a directory that joined only if the
@@ -1631,7 +1782,8 @@ module Rigor
1631
1782
  end
1632
1783
  end
1633
1784
 
1634
- attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths
1785
+ attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs, :deferred_signature_paths,
1786
+ :project_signature_paths, :core_root
1635
1787
 
1636
1788
  # @param libraries — stdlib library names to load on top of core (e.g.,
1637
1789
  # `["pathname", "json"]`). Empty by default. Each entry MUST correspond to a directory under the
@@ -1644,7 +1796,7 @@ module Rigor
1644
1796
  # `RbsEnvironment`, `RbsKnownClassNames`, `RbsConstantTable`, `RbsClassAncestorTable`, and
1645
1797
  # `RbsClassTypeParamNames` producers. Pass `nil` (the default) to skip caching; the runner threads
1646
1798
  # its own Store through here when enabled.
1647
- # @param virtual_rbs — >] ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1799
+ # @param virtual_rbs — ADR-32 WD4 — `[virtual_filename, rbs_source]` pairs
1648
1800
  # synthesised from project source by a plugin's `Manifest#source_rbs_synthesizer`. Merged into the
1649
1801
  # env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no
1650
1802
  # synthesizer-emitting plugin is loaded.
@@ -1652,10 +1804,15 @@ module Rigor
1652
1804
  # contributed (issue #610): loaded last and allowed to stand down against a colliding generic
1653
1805
  # arity. Read back by the env-cache producer, which rebuilds the env from the loader's readers on
1654
1806
  # a miss — a list it cannot read is a stand-down that runs only on a `--no-cache` build.
1655
- def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [],
1656
- deferred_signature_paths: [])
1807
+ # @param core_root — issue #2517 — a directory loaded in place of the `rbs` gem's `core/`, or nil (the
1808
+ # default) for that core. Validated here, so a bad selection fails at construction ({CoreRoot.validate!}).
1809
+ def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [], # rubocop:disable Metrics/ParameterLists
1810
+ deferred_signature_paths: [], project_signature_paths: [], required_features: [], core_root: nil)
1811
+ @core_root = core_root.nil? ? nil : CoreRoot.validate!(core_root)
1812
+ CoreRoot.reject_overlap!(@core_root, project_signature_paths) if @core_root
1657
1813
  @libraries = libraries.map(&:to_s).freeze
1658
1814
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
1815
+ assign_reopen_inputs(project_signature_paths, required_features)
1659
1816
  # Issue #610 — the subset of `signature_paths:` a bundled PLUGIN contributed. Loaded last and
1660
1817
  # allowed to stand down against a colliding generic arity ({.add_deferred_signatures}). Every entry
1661
1818
  # is also in `@signature_paths`, so the env cache descriptor already digests these files.
@@ -1723,8 +1880,14 @@ module Rigor
1723
1880
  # plugin sig collides, which is every project that has not run `rbs collection install` for a gem a
1724
1881
  # bundled plugin also declares. Reported as `rbs.coverage.plugin-signature-stood-down` (`:info`); the
1725
1882
  # same file MUST NOT also appear in {#quarantined_signatures}.
1883
+ #
1884
+ # Issue #2422 — followed by the members a deferred file that did load left out because another source
1885
+ # declares them ({.deferred_member_standdowns}), one row each.
1726
1886
  def signature_standdowns
1727
- @state[:signature_standdowns] ||= self.class.deferred_standdowns(env, @deferred_signature_paths).freeze
1887
+ @state[:signature_standdowns] ||= (
1888
+ self.class.deferred_standdowns(env, @deferred_signature_paths) +
1889
+ self.class.deferred_member_standdowns(env, @deferred_signature_paths)
1890
+ ).freeze
1728
1891
  end
1729
1892
 
1730
1893
  # Issue #1713 — what stood down from each {RequiredFeatures}-gated vendored directory this loader's library
@@ -1764,6 +1927,9 @@ module Rigor
1764
1927
  # a partial sidecar as authoritative made `sig-gen --write` on redmine report 5.9x more
1765
1928
  # `call.undefined-method` than the same project with no `sig/` at all.
1766
1929
  #
1930
+ # A class declared only by RBS synthesized from the project's own source joins the set too
1931
+ # ({#inline_only_entry?}, issue #1934).
1932
+ #
1767
1933
  # Memoised per loader: one pass over `class_decls` (~1-2k entries), and every consumer is a check
1768
1934
  # rule on the analysis hot path. Attribution is by buffer NAME, which survives the ADR-54 environment
1769
1935
  # cache since #725 — an environment blob written before that carries the `<cached>` sentinel instead,
@@ -1777,10 +1943,14 @@ module Rigor
1777
1943
  return Set.new.freeze if environment.nil?
1778
1944
 
1779
1945
  project_files = self.class.project_sig_files(@signature_paths)
1780
- return Set.new.freeze if project_files.empty?
1946
+ virtual_names = @virtual_rbs.to_set(&:first)
1947
+ return Set.new.freeze if project_files.empty? && virtual_names.empty?
1781
1948
 
1782
1949
  names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1783
- acc << rbs_name.to_s.delete_prefix("::") if self.class.project_entry?(entry, project_files)
1950
+ next unless self.class.project_entry?(entry, project_files) ||
1951
+ inline_only_entry?(entry, virtual_names, project_files)
1952
+
1953
+ acc << rbs_name.to_s.delete_prefix("::")
1784
1954
  end
1785
1955
  names.freeze
1786
1956
  rescue ::RBS::BaseError
@@ -1788,6 +1958,148 @@ module Rigor
1788
1958
  end
1789
1959
  private :build_project_declared_classes
1790
1960
 
1961
+ # Issue #1934 — a class whose every declaration a source RBS synthesizer (`rigor-rbs-inline`) wrote from
1962
+ # the project's own `.rb` files is the project's as much as a `sig/` sidecar is: the declaration is the
1963
+ # inline comments of the source under analysis, so a method another project file adds to the class (a
1964
+ # plain `def`, or an `alias_method` whose target the inline declaration names) is the second file of an
1965
+ # ordinary class, not an ADR-17 monkey-patch. Reading the synthesized RBS as bundled reported
1966
+ # `alias_method :title, :label` from another file while the `alias` keyword, which upstream translates,
1967
+ # read clean.
1968
+ #
1969
+ # EVERY declaration, not only the primary one, and only virtual or `sig/` buffers: a core, stdlib or gem
1970
+ # class an inline-annotated file reopens (`class String` with a `#:` method) keeps its bundled
1971
+ # declaration, and a project `def` on it stays the monkey-patch ADR-17 reports. A declaration whose
1972
+ # buffer cannot be attributed (the pre-#725 `<cached>` sentinel) fails the test, which keeps the
1973
+ # pre-#1934 reading.
1974
+ def inline_only_entry?(entry, virtual_names, project_files)
1975
+ return false if virtual_names.empty?
1976
+
1977
+ project_authored_entry?(entry, virtual_names, project_files)
1978
+ end
1979
+ private :inline_only_entry?
1980
+
1981
+ # Every declaration of `entry` sits in a project `sig/` file or in RBS synthesized from the project's
1982
+ # source; a declaration whose buffer cannot be attributed fails the test.
1983
+ def project_authored_entry?(entry, virtual_names, project_files)
1984
+ buffers = self.class.entry_declarations(entry).map { |decl| self.class.declaration_buffer_name(decl) }
1985
+ return false if buffers.empty? || buffers.any?(&:nil?)
1986
+
1987
+ buffers.all? { |name| virtual_names.include?(name) || project_files.include?(File.absolute_path(name)) }
1988
+ end
1989
+ private :project_authored_entry?
1990
+
1991
+ # Issue #2174 — true when every RBS declaration of `class_name` is the project's own (rbs-inline `#:`
1992
+ # annotations, or a file under the configured `signature_paths:`) and its root constant belongs to a library
1993
+ # Ruby ships ({RubyLibraryRoots}) that the project's source requires ({RequiredFeatures.features_of}). Such
1994
+ # a declaration reopens a library class whose real signatures did not load (`class Matrix` with one
1995
+ # annotated method beside `require "matrix"`, and no `matrix` RBS), so it lists only the methods the
1996
+ # project added: the class's surface is not enumerable from it, and a call it omits is not evidence of an
1997
+ # undefined method. A library whose RBS did load contributes a declaration of its own (core, stdlib, a gem's
1998
+ # `sig/`, an rbs collection, a plugin's `sig/`), which takes the class out of this predicate. A project
1999
+ # class that shares a required library's root name declines too; that is the accepted cost.
2000
+ def ruby_library_reopen?(class_name)
2001
+ name = class_name.to_s.delete_prefix("::")
2002
+ return false if @required_features.empty? || !project_authored_classes.include?(name)
2003
+
2004
+ segments = ruby_library_roots[name.split("::").first]
2005
+ !segments.nil? && @required_features.any? { |feature| segments.include?(feature.split("/").first) }
2006
+ end
2007
+
2008
+ def ruby_library_roots
2009
+ @state[:ruby_library_roots] ||= RubyLibraryRoots.shared
2010
+ end
2011
+
2012
+ # Issue #2174 — the subset of `signature_paths:` the user configured (every entry is also in
2013
+ # `@signature_paths`), and the features the project's source requires: read only by
2014
+ # {#ruby_library_reopen?}.
2015
+ def assign_reopen_inputs(project_signature_paths, required_features)
2016
+ @project_signature_paths = project_signature_paths.map { |p| Pathname(p) }.freeze
2017
+ @required_features = required_features.map { |feature| feature.to_s.dup.freeze }.freeze
2018
+ end
2019
+ private :assign_reopen_inputs
2020
+ private :ruby_library_roots
2021
+
2022
+ # Issue #2438 — true when every RBS declaration of `class_name` is the project's own
2023
+ # ({#project_authored_classes}) and none of them declares a member of the class itself: no method, attribute,
2024
+ # alias or mixin, only nested class / module / constant / type declarations (and variable or visibility
2025
+ # lines, which add no method). `rigor sig-gen --write` writes exactly this for a namespace whose source
2026
+ # lists no methods of its own (`module NS; class Foo; …; end; end`), while the namespace's methods live
2027
+ # where the generator cannot see them: a file outside `paths:`, a mixin, a hook. Such a declaration
2028
+ # names the class without describing its surface. Bundled (core, stdlib, gem) declarations never count.
2029
+ def memberless_project_class?(class_name)
2030
+ memberless_project_classes.key?(class_name.to_s.delete_prefix("::"))
2031
+ end
2032
+
2033
+ # The superclass a member-less project declaration of `class_name` writes (`class Poll < ::ApplicationRecord`),
2034
+ # as RBS spells it (rooted), or nil when none of its declarations writes one or the class is not member-less.
2035
+ def memberless_project_superclass(class_name)
2036
+ memberless_project_classes[class_name.to_s.delete_prefix("::")]
2037
+ end
2038
+
2039
+ def memberless_project_classes
2040
+ @state[:memberless_project_classes] ||= build_memberless_project_classes
2041
+ end
2042
+ private :memberless_project_classes
2043
+
2044
+ SURFACE_MEMBERS = [
2045
+ ::RBS::AST::Members::MethodDefinition, ::RBS::AST::Members::Attribute,
2046
+ ::RBS::AST::Members::Alias, ::RBS::AST::Members::Mixin
2047
+ ].freeze
2048
+ private_constant :SURFACE_MEMBERS
2049
+
2050
+ def build_memberless_project_classes
2051
+ authored = project_authored_classes
2052
+ environment = env
2053
+ return {}.freeze if authored.empty? || environment.nil?
2054
+
2055
+ environment.class_decls.each_with_object({}) do |(rbs_name, entry), acc|
2056
+ name = rbs_name.to_s.delete_prefix("::")
2057
+ next unless authored.include?(name)
2058
+
2059
+ decls = self.class.entry_declarations(entry)
2060
+ acc[name] = declared_superclass_name(decls) if decls.none? { |decl| surface_member?(decl) }
2061
+ end.freeze
2062
+ rescue ::RBS::BaseError
2063
+ {}.freeze
2064
+ end
2065
+
2066
+ def surface_member?(decl)
2067
+ members = decl.respond_to?(:members) ? decl.members : []
2068
+ members.any? { |member| SURFACE_MEMBERS.any? { |kind| member.is_a?(kind) } }
2069
+ end
2070
+ private :surface_member?
2071
+
2072
+ def declared_superclass_name(decls)
2073
+ decls.filter_map { |decl| decl.respond_to?(:super_class) && decl.super_class&.name&.to_s }.first&.freeze
2074
+ end
2075
+ private :declared_superclass_name
2076
+ private :build_memberless_project_classes
2077
+
2078
+ # The class / module names every one of whose declarations is the project's own
2079
+ # ({#project_authored_entry?}, which {#inline_only_entry?} asks behind its inline-only gate).
2080
+ def project_authored_classes
2081
+ @state[:project_authored_classes] ||= build_project_authored_classes
2082
+ end
2083
+ private :project_authored_classes
2084
+
2085
+ def build_project_authored_classes
2086
+ environment = env
2087
+ return Set.new.freeze if environment.nil?
2088
+
2089
+ # Only the user's configured `signature_paths:`: `@signature_paths` also holds plugin, gem-shipped,
2090
+ # rbs-collection and overlay directories, whose declarations are a library's own.
2091
+ project_files = self.class.project_sig_files(@project_signature_paths)
2092
+ virtual_names = @virtual_rbs.to_set(&:first)
2093
+ return Set.new.freeze if project_files.empty? && virtual_names.empty?
2094
+
2095
+ environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
2096
+ acc << rbs_name.to_s.delete_prefix("::") if project_authored_entry?(entry, virtual_names, project_files)
2097
+ end.freeze
2098
+ rescue ::RBS::BaseError
2099
+ Set.new.freeze
2100
+ end
2101
+ private :build_project_authored_classes
2102
+
1791
2103
  # Issue #527 — the class / module names whose PRIMARY declaration lives in the `rbs` gem's own
1792
2104
  # `core/` or `stdlib/` tree, top-level prefix stripped.
1793
2105
  #
@@ -1808,9 +2120,39 @@ module Rigor
1808
2120
  @state[:core_stdlib_classes] ||= build_core_stdlib_classes
1809
2121
  end
1810
2122
 
1811
- def build_core_stdlib_classes
2123
+ # Issue #1154 — the subset of {#core_stdlib_classes} whose primary declaration is in `core/` alone: the classes
2124
+ # the interpreter implements, as opposed to a stdlib library written in Ruby (`Logger` rewrites `@level` in
2125
+ # its `initialize`). Same attribution, same cache behaviour.
2126
+ def core_class?(class_name)
2127
+ (@state[:core_classes] ||= build_core_stdlib_classes(core_roots)).include?(class_name.to_s.delete_prefix("::"))
2128
+ end
2129
+
2130
+ # Issue #2517 — the `core/` prefixes a declaration's buffer name is tested against: the selected
2131
+ # {#core_root} when there is one, which is what the environment loaded core from, else the `rbs` gem's.
2132
+ def core_roots
2133
+ @core_root ? ["#{@core_root}/".freeze].freeze : CORE_ROOTS
2134
+ end
2135
+ private :core_roots
2136
+
2137
+ # {CORE_STDLIB_ROOTS} with {#core_roots} in place of the `rbs` gem's `core/`.
2138
+ def core_stdlib_roots
2139
+ return CORE_STDLIB_ROOTS unless @core_root
2140
+ return [].freeze if CORE_STDLIB_ROOTS.empty?
2141
+
2142
+ (core_roots + (CORE_STDLIB_ROOTS - CORE_ROOTS)).freeze
2143
+ end
2144
+ private :core_stdlib_roots
2145
+
2146
+ CORE_ROOTS = begin
2147
+ [::RBS::EnvironmentLoader::DEFAULT_CORE_ROOT].compact.map { |root| "#{root}/".freeze }.freeze
2148
+ rescue StandardError
2149
+ [].freeze
2150
+ end
2151
+ private_constant :CORE_ROOTS
2152
+
2153
+ def build_core_stdlib_classes(roots = core_stdlib_roots)
1812
2154
  environment = env
1813
- return Set.new.freeze if environment.nil? || CORE_STDLIB_ROOTS.empty?
2155
+ return Set.new.freeze if environment.nil? || roots.empty?
1814
2156
 
1815
2157
  names = environment.class_decls.each_with_object(Set.new) do |(rbs_name, entry), acc|
1816
2158
  decl = self.class.primary_decl_for(entry)
@@ -1819,7 +2161,7 @@ module Rigor
1819
2161
  path = self.class.declaration_buffer_name(decl)
1820
2162
  next if path.nil?
1821
2163
 
1822
- acc << rbs_name.to_s.delete_prefix("::") if CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }
2164
+ acc << rbs_name.to_s.delete_prefix("::") if roots.any? { |root| path.start_with?(root) }
1823
2165
  end
1824
2166
  names.freeze
1825
2167
  rescue ::RBS::BaseError
@@ -1868,7 +2210,7 @@ module Rigor
1868
2210
  next if type.is_a?(Type::Bot)
1869
2211
 
1870
2212
  path = self.class.declaration_buffer_name(entry.decl)
1871
- acc[name.to_sym] = [type, !path.nil? && CORE_STDLIB_ROOTS.any? { |root| path.start_with?(root) }].freeze
2213
+ acc[name.to_sym] = [type, !path.nil? && core_stdlib_roots.any? { |root| path.start_with?(root) }].freeze
1872
2214
  end
1873
2215
  table.freeze
1874
2216
  rescue ::RBS::BaseError
@@ -1892,6 +2234,13 @@ module Rigor
1892
2234
  @state[:env_build_failure]
1893
2235
  end
1894
2236
 
2237
+ # Issue #2517 — builds the environment now when a core is selected, so a core it does not build from raises
2238
+ # its {CoreRootError} at the caller rather than at the first query inside analysis. No-op for the default core.
2239
+ def verify_core_root!
2240
+ env if @core_root
2241
+ nil
2242
+ end
2243
+
1895
2244
  # Issue #696 — the PER-CLASS sibling of {#env_build_failure}, one tier quieter in consequence: an
1896
2245
  # `RBS::DefinitionBuilder` failure ({#build_instance_definition} / {#build_singleton_definition}'s
1897
2246
  # rescue) leaves the class KNOWN but with no method surface, so every call on it — real methods and
@@ -1977,7 +2326,8 @@ module Rigor
1977
2326
  def member_consistency(proof: nil)
1978
2327
  key = proof ? :member_consistency_proven : :member_consistency
1979
2328
  @state[key] ||= self.class.member_consistency_for(
1980
- self.class.project_sig_files(@signature_paths), @virtual_rbs, proof: proof
2329
+ self.class.project_sig_files(@signature_paths) - self.class.project_sig_files(@deferred_signature_paths),
2330
+ @virtual_rbs, proof: proof
1981
2331
  ).records
1982
2332
  end
1983
2333
 
@@ -2105,6 +2455,32 @@ module Rigor
2105
2455
  end
2106
2456
  private :mixin_parent
2107
2457
 
2458
+ # Issue #2321 — every class name an RBS class declaration names as its superclass, `::`-stripped: a class
2459
+ # absent from the set has no RBS-declared subclass. `nil` when the environment is unavailable or fails to read,
2460
+ # so a caller cannot mistake an unread environment for one without subclasses. Memoised per loader.
2461
+ def superclass_names
2462
+ return @state[:superclass_names] if @state.key?(:superclass_names)
2463
+
2464
+ @state[:superclass_names] = build_superclass_names&.freeze
2465
+ end
2466
+
2467
+ def build_superclass_names
2468
+ return nil if env.nil?
2469
+
2470
+ names = Set.new
2471
+
2472
+ env.class_decls.each_value do |entry|
2473
+ self.class.entry_declarations(entry).each do |decl|
2474
+ superclass = decl.respond_to?(:super_class) ? decl.super_class : nil
2475
+ names << superclass.name.to_s.delete_prefix("::") if superclass
2476
+ end
2477
+ end
2478
+ names
2479
+ rescue ::RBS::BaseError
2480
+ nil
2481
+ end
2482
+ private :build_superclass_names
2483
+
2108
2484
  # Yields every known class / module / alias name (top-level prefixed) currently loaded into the
2109
2485
  # environment. The cache producer that materialises the known-name set uses this so it never recurses
2110
2486
  # back through {#class_known?}.
@@ -2121,7 +2497,7 @@ module Rigor
2121
2497
  end
2122
2498
 
2123
2499
  # Yields every type-alias declaration loaded into the environment (`type foo = ...`).
2124
- # Yields `(RBS::TypeName, RBS::Environment::TypeAliasEntry)` pairs.
2500
+ # Yields `[type_name, decl_entry]` pairs.
2125
2501
  def each_type_alias_decl
2126
2502
  return enum_for(:each_type_alias_decl) unless block_given?
2127
2503
  return unless env
@@ -2179,7 +2555,7 @@ module Rigor
2179
2555
  # fail-soft: see #each_class_decl_annotation.
2180
2556
  end
2181
2557
 
2182
- # Returns a frozen `Hash<String, String>` mapping each loaded class / module name (top-level prefixed)
2558
+ # Returns a frozen mapping from each loaded class / module name (top-level prefixed)
2183
2559
  # to the file path of its FIRST declaration's RBS source. Used by {Rigor::Analysis::RunStats} to
2184
2560
  # attribute the type universe between "project sig/" (paths under the configured `signature_paths`)
2185
2561
  # and "bundled" (everything else — RBS core, stdlib libraries, gem-bundled RBS). Each value is a frozen
@@ -2400,7 +2776,7 @@ module Rigor
2400
2776
  # singleton methods (e.g., `Array.new`) parameterize over the same `Elem` as instance methods.
2401
2777
  #
2402
2778
  # Returns an empty array for non-generic classes and for unknown names (the loader stays fail-soft).
2403
- # NOTE: in the `rbs` gem, `RBS::Definition#type_params` returns `Array<Symbol>` directly, not the AST
2779
+ # NOTE: in the `rbs` gem, `RBS::Definition#type_params` returns the names directly, not the AST
2404
2780
  # `TypeParam` object (those live on the AST level).
2405
2781
  #
2406
2782
  # When `cache_store` is set, the loader fetches the entire type-parameter-name table once (per
@@ -2430,6 +2806,31 @@ module Rigor
2430
2806
  NO_TYPE_PARAMS = [].freeze
2431
2807
  private_constant :NO_TYPE_PARAMS
2432
2808
 
2809
+ # Issue #2436 — the `RBS::AST::TypeParam` list a loaded declaration of `class_name` spells (`[unchecked out
2810
+ # E]` for `Array`), read off the declarations rather than through {#class_type_param_names}: a definition
2811
+ # build validates the entry and raises on one whose declarations already disagree, which is the env sig-gen
2812
+ # is asked to repair. The first declaration answers, as it is the one rbs compares every other against
2813
+ # ({.generic_arity_conflict}). Empty for a non-generic or unknown name, and when the environment did not
2814
+ # build.
2815
+ def class_declaration_type_params(class_name)
2816
+ memo = (@state[:declaration_type_params] ||= {})
2817
+ key = class_name.to_s.delete_prefix("::")
2818
+ memo.fetch(key) { memo[key] = read_declaration_type_params(key) }
2819
+ end
2820
+
2821
+ def read_declaration_type_params(key)
2822
+ rbs_name = parse_type_name(key)
2823
+ entry = rbs_name && env&.class_decls&.[](rbs_name)
2824
+ return NO_TYPE_PARAMS if entry.nil?
2825
+
2826
+ first = self.class.entry_declarations(entry).first
2827
+ params = first.respond_to?(:type_params) ? first.type_params : nil
2828
+ params.nil? || params.empty? ? NO_TYPE_PARAMS : params.dup.freeze
2829
+ rescue ::RBS::BaseError
2830
+ NO_TYPE_PARAMS
2831
+ end
2832
+ private :read_declaration_type_params
2833
+
2433
2834
  def class_ordering(lhs, rhs)
2434
2835
  @hierarchy.class_ordering(lhs, rhs)
2435
2836
  end
@@ -2702,7 +3103,7 @@ module Rigor
2702
3103
  producer.send(:compute, self)
2703
3104
  end
2704
3105
 
2705
- # ADR-15 Phase 2b — `Hash<String, Array<String>>` of normalised ancestor chains per class. Consumes
3106
+ # ADR-15 Phase 2b — the normalised ancestor chain of each class. Consumes
2706
3107
  # the existing `RbsClassAncestorTable` producer when `cache_store` is set; falls back to the
2707
3108
  # producer's `compute` otherwise. Used by {#reflection}.
2708
3109
  def ancestor_names_table
@@ -2737,6 +3138,7 @@ module Rigor
2737
3138
  # per AST node touched during analysis, multiplying per-file analysis cost by ~100x. Failures
2738
3139
  # short-circuit to `nil` here and are surfaced to the user via `warn_about_env_build_failure_once`.
2739
3140
  def env
3141
+ raise @state[:core_root_error] if @state[:core_root_error]
2740
3142
  return @state[:env] if @state[:env_loaded]
2741
3143
 
2742
3144
  @state[:env_loaded] = true
@@ -2745,11 +3147,24 @@ module Rigor
2745
3147
  warn_about_virtual_rbs_collisions
2746
3148
  @state[:env]
2747
3149
  rescue ::RBS::BaseError => e
3150
+ # Issue #2517 — an environment that does not build from an explicitly selected core is not analysed
3151
+ # against at all: the degraded `Dynamic[top]` env would report the very APIs the selection removes as
3152
+ # fine. Memoised like the nil env, so every later reader raises the same error instead of rebuilding.
3153
+ raise(@state[:core_root_error] = core_root_build_error(e)) if @core_root
3154
+
2748
3155
  record_env_build_failure(e)
2749
3156
  warn_about_env_build_failure_once(e)
2750
3157
  @state[:env] = nil
2751
3158
  end
2752
3159
 
3160
+ def core_root_build_error(error)
3161
+ CoreRootError.new(
3162
+ "core_root: #{@core_root.inspect} did not build an RBS environment " \
3163
+ "(#{error.class.name}: #{error.message.lines.first.to_s.strip})"
3164
+ )
3165
+ end
3166
+ private :core_root_build_error
3167
+
2753
3168
  # Tell the user, once per run, which of their `signature_paths:` `.rbs` files were skipped because they
2754
3169
  # do not parse ({RbsLoader.add_project_signatures} quarantines them so the rest of the env survives). A
2755
3170
  # skipped file's types are absent, so calls into them read `Dynamic[top]` — silently, without this. This
@@ -2857,7 +3272,7 @@ module Rigor
2857
3272
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
2858
3273
  end
2859
3274
 
2860
- # Full `Hash<String, RBS::Definition>` tables for the {#prewarm} / {#reflection} consumers (ADR-15
3275
+ # Full tables of `RBS::Definition` per class for the {#prewarm} / {#reflection} consumers (ADR-15
2861
3276
  # Phase 2b/4b.x), which need every definition materialised up front. Built from the (cached) env via
2862
3277
  # `RBS::DefinitionBuilder` — ADR-54 WD1 retired the disk blobs these used to `Marshal.load` (building
2863
3278
  # all definitions from a cached env is faster), so the eager-table cost is now a compute, not a load.
@@ -2898,7 +3313,9 @@ module Rigor
2898
3313
  libraries: @libraries,
2899
3314
  signature_paths: @signature_paths,
2900
3315
  virtual_rbs: @virtual_rbs,
2901
- deferred_signature_paths: @deferred_signature_paths
3316
+ deferred_signature_paths: @deferred_signature_paths,
3317
+ project_signature_paths: @project_signature_paths,
3318
+ core_root: @core_root
2902
3319
  )
2903
3320
  end
2904
3321
 
@@ -2925,6 +3342,15 @@ module Rigor
2925
3342
  rbs_name = canonical_module_name(rbs_name)
2926
3343
  return nil unless env.class_decls.key?(rbs_name)
2927
3344
 
3345
+ # rbs's `AncestorBuilder#singleton_ancestors` has no cycle check: a class whose superclass chain loops
3346
+ # (`class S < ::S`, which a collection copy of the same class used to hide, #2441) recurses until the stack
3347
+ # runs out and aborts the run, where `#instance_ancestors` raises `RBS::RecursiveAncestorError`. Walking the
3348
+ # instance side first turns the loop into that error, so it is this class's build failure. Memoised by the
3349
+ # builder, so a class whose instance side was already built pays nothing. Any other stack exhaustion stays
3350
+ # fatal: it depends on how deep the analysis already was, and must not be memoised as a build failure.
3351
+ if env.class_decls[rbs_name].is_a?(::RBS::Environment::ClassEntry)
3352
+ builder.ancestor_builder.instance_ancestors(rbs_name)
3353
+ end
2928
3354
  builder.build_singleton(rbs_name)
2929
3355
  rescue ::RBS::BaseError => e
2930
3356
  store_definition_build_detail(class_name, e)
@@ -2954,7 +3380,8 @@ module Rigor
2954
3380
  key = class_name.to_s.delete_prefix("::")
2955
3381
  details = ((@state[:definition_build_details] ||= {})[key] ||= [])
2956
3382
  details << [key, error.class.name.to_s, definition_build_member(error),
2957
- definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
3383
+ definition_build_conflict_buffers(error, key), unresolved_type_name_detail(error),
3384
+ variable_mixin_count(error, key)].freeze
2958
3385
  end
2959
3386
 
2960
3387
  # Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
@@ -3105,7 +3532,7 @@ module Rigor
3105
3532
  end
3106
3533
 
3107
3534
  # The third twin of {#warn_about_quarantined_signatures} / {#warn_about_virtual_rbs_collisions}: name,
3108
- # once per class per PROCESS, a `RBS::DefinitionBuilder` failure caught in
3535
+ # once per class per loader, a `RBS::DefinitionBuilder` failure caught in
3109
3536
  # {#build_instance_definition} / {#build_singleton_definition}'s rescue. Without it, `class_known?`
3110
3537
  # stays true (it only consults {#known_class_names_set}, never a definition build), so every call on
3111
3538
  # the class — real methods and typos alike — silently degrades to `Dynamic[top]`.
@@ -3142,14 +3569,14 @@ module Rigor
3142
3569
 
3143
3570
  def definition_build_failure_warning(class_name, error)
3144
3571
  first_line = error.message.to_s.lines.first.to_s.strip
3145
- buffers = definition_build_conflict_buffers(error)
3572
+ buffers = definition_build_conflict_buffers(error, class_name)
3146
3573
  collisions =
3147
3574
  if buffers.empty?
3148
3575
  ""
3149
3576
  else
3150
3577
  listed = buffers.first(QUARANTINE_WARN_LIMIT)
3151
3578
  more = buffers.size - listed.size
3152
- lines = listed.map { |name| " - #{name}" }
3579
+ lines = listed.map { |name| " - #{signature_source_label(name)}" }
3153
3580
  lines << " … and #{more} more" if more.positive?
3154
3581
  "\n Colliding declaration(s):\n#{lines.join("\n")}"
3155
3582
  end
@@ -3158,6 +3585,14 @@ module Rigor
3158
3585
  "`Dynamic[top]` — real methods and typos alike — instead of resolving normally.#{collisions}"
3159
3586
  end
3160
3587
 
3588
+ # Issue #2440 — a `virtual:<plugin>:<path>` buffer named by the `.rb` file it was synthesized from.
3589
+ def signature_source_label(name)
3590
+ prefix, plugin_id, path = name.to_s.split(":", 3)
3591
+ return name.to_s unless prefix == "virtual" && plugin_id && path && !path.empty?
3592
+
3593
+ "#{path} (inline RBS via #{plugin_id})"
3594
+ end
3595
+
3161
3596
  def warn_about_deferred_definition_build_failures
3162
3597
  return if @state[:definition_build_summary_warned]
3163
3598
 
@@ -3196,10 +3631,11 @@ module Rigor
3196
3631
  # `InvalidOverloadMethodError` carry `#members` (each with a `#location`);
3197
3632
  # `DuplicatedInterfaceMethodDefinitionError` carries a single `#member`; most others
3198
3633
  # (`NoSuperclassFoundError`, `UnknownMethodAliasError`, `RecursiveAncestorError`, …) carry only a bare
3199
- # `#location`. Falls back to an empty list — the warning still names the class and the exception's own
3200
- # message — for the remainder (e.g. `SuperclassMismatchError`, which carries neither).
3201
- def definition_build_conflict_buffers(error)
3202
- definition_build_error_locations(error)
3634
+ # `#location`. A duplicated instance variable carries one location and a `SuperclassMismatchError` none,
3635
+ # so both read the class entry's declarations for the other side (#2440). Falls back to an empty list —
3636
+ # the warning still names the class and the exception's own message — for the remainder.
3637
+ def definition_build_conflict_buffers(error, class_name = nil)
3638
+ definition_build_error_locations(error, class_name)
3203
3639
  .compact.filter_map { |loc| loc.buffer&.name }.map(&:to_s)
3204
3640
  .reject { |name| name == CACHED_LOCATION_BUFFER_NAME }.uniq.freeze
3205
3641
  rescue ::RBS::BaseError, StandardError
@@ -3208,14 +3644,100 @@ module Rigor
3208
3644
 
3209
3645
  # The `RBS::Location`s an error carries, by whichever accessor its class happens to expose (see the
3210
3646
  # shape survey above). Split out so the buffer-name extraction reads as one pipeline.
3211
- def definition_build_error_locations(error)
3647
+ def definition_build_error_locations(error, class_name = nil)
3212
3648
  return Array(error.members).map(&:location) if error.respond_to?(:members) && error.members
3213
3649
  return [error.member.location] if error.respond_to?(:member) && error.member
3650
+ # `VariableDuplicationError` is younger than the rbs floor the gemspec allows.
3651
+ if defined?(::RBS::VariableDuplicationError) && error.is_a?(::RBS::VariableDuplicationError)
3652
+ return variable_duplication_locations(error, class_name)
3653
+ end
3654
+ return superclass_mismatch_locations(error) if error.is_a?(::RBS::SuperclassMismatchError)
3214
3655
  return [error.location] if error.respond_to?(:location)
3215
3656
 
3216
3657
  []
3217
3658
  end
3218
3659
 
3660
+ # Issue #2440 — a duplicated `@x` / `self.@x` carries the location of ONE of its two declarations, so
3661
+ # the file list named only that side (typically the `virtual:rbs-inline:` buffer, leaving out the
3662
+ # committed `.rbs` the user can actually edit). The class entry's declarations name the rest: every
3663
+ # declaration of the class that declares the same variable, of the same kind. When the variable's
3664
+ # owner is a module the failing class mixes in (rbs counts it once per declaration that does), the
3665
+ # declarations of the failing class that mix that module in are named too: they are the duplicate.
3666
+ def variable_duplication_locations(error, class_name = nil)
3667
+ [error.location, *variable_declarations(error).map(&:location), *mixin_locations(class_name, error.type_name)]
3668
+ end
3669
+
3670
+ # Every declaration of the variable the error names, of the error's kind, in its owner's entry.
3671
+ def variable_declarations(error)
3672
+ member_class =
3673
+ if error.is_a?(::RBS::ClassInstanceVariableDuplicationError)
3674
+ ::RBS::AST::Members::ClassInstanceVariable
3675
+ else
3676
+ ::RBS::AST::Members::InstanceVariable
3677
+ end
3678
+ class_entry_decls(error.type_name).flat_map do |decl|
3679
+ decl_members(decl).select { |m| m.instance_of?(member_class) && m.name == error.variable_name }
3680
+ end
3681
+ end
3682
+
3683
+ # Issue #2440 — how many times `class_name`'s own declarations mix in the module that owns the
3684
+ # duplicated variable, when that is what duplicated it: the owner is another module, it declares the
3685
+ # variable exactly once, and the class mixes it in at least twice (in one declaration or across
3686
+ # several). nil otherwise — a variable duplicated in the owner itself and inherited (`Sub < Base`)
3687
+ # is not a double mixin. Carried in the failure tuple's sixth slot so the advice can tell the two
3688
+ # apart on a replayed tuple too.
3689
+ def variable_mixin_count(error, class_name)
3690
+ return nil unless defined?(::RBS::VariableDuplicationError) && error.is_a?(::RBS::VariableDuplicationError)
3691
+
3692
+ owner = parse_type_name(class_name)
3693
+ return nil if owner.nil? || owner == error.type_name
3694
+ return nil unless variable_declarations(error).size == 1
3695
+
3696
+ count = class_entry_decls(owner).sum do |decl|
3697
+ decl_members(decl).count { |m| mixin_of?(m, error.type_name) }
3698
+ end
3699
+ count >= 2 ? count : nil
3700
+ rescue ::RBS::BaseError, StandardError
3701
+ nil
3702
+ end
3703
+
3704
+ MIXIN_MEMBERS = [::RBS::AST::Members::Include, ::RBS::AST::Members::Extend,
3705
+ ::RBS::AST::Members::Prepend].freeze
3706
+ private_constant :MIXIN_MEMBERS
3707
+
3708
+ # `resolve_type_names` has made every mixin name absolute.
3709
+ def mixin_of?(member, module_name)
3710
+ MIXIN_MEMBERS.any? { |klass| member.is_a?(klass) } && member.name == module_name
3711
+ end
3712
+
3713
+ def mixin_locations(class_name, module_name)
3714
+ return [] if class_name.nil?
3715
+
3716
+ owner = parse_type_name(class_name)
3717
+ return [] if owner.nil? || owner == module_name
3718
+
3719
+ class_entry_decls(owner).filter_map do |decl|
3720
+ decl.location if decl_members(decl).any? { |m| mixin_of?(m, module_name) }
3721
+ end
3722
+ end
3723
+
3724
+ def decl_members(decl)
3725
+ decl.respond_to?(:members) ? Array(decl.members) : []
3726
+ end
3727
+
3728
+ # Issue #2440 — `SuperclassMismatchError` carries the class entry and no location of its own: the
3729
+ # colliding declarations are the ones that name a superclass. {.entry_declarations} reads the entry on
3730
+ # both rbs majors (`each_decl` on 4.x, `MultiEntry::D` wrappers on 3.x).
3731
+ def superclass_mismatch_locations(error)
3732
+ decls = self.class.entry_declarations(error.entry)
3733
+ decls.select { |decl| decl.respond_to?(:super_class) && decl.super_class }.map(&:location)
3734
+ end
3735
+
3736
+ def class_entry_decls(type_name)
3737
+ entry = env&.class_decls&.[](type_name)
3738
+ entry.nil? ? [] : self.class.entry_declarations(entry)
3739
+ end
3740
+
3219
3741
  # The member the failure is ABOUT, taken off the error object rather than parsed out of its message.
3220
3742
  #
3221
3743
  # Issue #696 — the message cannot serve here. It is built from `RBS::Location`s, and the ADR-54 env