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
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../configuration"
6
6
  require_relative "../environment"
7
+ require_relative "../run_scoped_slots"
7
8
  require_relative "../project_environment"
8
9
  require_relative "../scope"
9
10
  require_relative "../reflection"
@@ -16,6 +17,7 @@ require_relative "../inference/def_return_typer"
16
17
  require_relative "../inference/scope_indexer"
17
18
  require_relative "../inference/project_seed_tables"
18
19
  require_relative "../inference/module_function_state"
20
+ require_relative "bot_proposal_guard"
19
21
  require_relative "../inference/rbs_type_translator"
20
22
  require_relative "alias_index"
21
23
  require_relative "meta_class_shape"
@@ -26,6 +28,8 @@ require_relative "declaration_equivalence"
26
28
  require_relative "inline_declarations"
27
29
  require_relative "layout_index"
28
30
  require_relative "method_candidate"
31
+ require_relative "platform_guarded_files"
32
+ require_relative "type_name_qualification"
29
33
  require_relative "writer"
30
34
  require_relative "../rooted_glob"
31
35
 
@@ -79,16 +83,20 @@ module Rigor
79
83
  attr_reader :unrenderable, :unresolvable_superclasses
80
84
 
81
85
  # @param paths — files / directories to scan.
82
- # @param observations — => Array<Array<Rigor::Type>>}]
86
+ # @param observations —
83
87
  # ADR-14 slice 3 — per-target-method arg-tuple observations
84
88
  # produced by {ObservationCollector}. An empty Hash (the default)
85
89
  # means "no observations available; emit `untyped` for every
86
90
  # parameter position" per ADR-5 clause 2.
91
+ # @param target_writer — the {Writer} a `--write` or `--check` run hands the candidates to. When given, a
92
+ # class counts as written for a subclass only if that writer accepts its target file (#2461); nil (print,
93
+ # diff, JSON) counts every class with a candidate the writer is sure to write.
87
94
  # @param overwrite — `--overwrite`: a member declared inline whose `sig/` copy disagrees is proposed as an
88
95
  # `inline-overwrite` of the whole member instead of refused (ADR-112 WD4).
89
96
  def initialize(configuration:, paths:, observations: {}, include_private: false, effect_annotator: nil,
90
- overwrite: false)
97
+ overwrite: false, target_writer: nil)
91
98
  @configuration = configuration
99
+ @target_writer = target_writer
92
100
  @overwrite = overwrite
93
101
  @paths = paths
94
102
  @observations = normalize_observations(observations)
@@ -114,6 +122,36 @@ module Rigor
114
122
  @unresolvable_superclasses = {}
115
123
  # Issue #722 — whole-run: `{ class name => the lexical nesting its header is written in }`.
116
124
  @superclass_nestings = {}
125
+ # Issue #2437 — whole-run: every class a walked file declares with a `class` keyword or a `Data.define` /
126
+ # `Struct.new` layout, so the report can tell a project superclass this run did not write from one nothing
127
+ # declares.
128
+ @source_classes = Set.new
129
+ end
130
+
131
+ # How the report names an unresolvable superclass token ({#unresolvable_superclasses}). It is a project
132
+ # superclass when it names a class the walked source declares and this run wrote nothing for, whose own chain
133
+ # would close if the project's classes were written:
134
+ # each step a source class, ending at no superclass or at one the RBS environment knows (#2437). What is
135
+ # missing is then the project's own declaration, not a gem's RBS. `class ApplicationRecord <
136
+ # ActiveRecord::Base` with no RBS for the gem is not such a class: writing it would not help.
137
+ #
138
+ # Returns `[:project, token]` for such a class. Otherwise `[:external, root]`, where `root` is the first token
139
+ # on the chain that is not a source class: `User < ApplicationRecord` reports `ActiveRecord::Base`, the class
140
+ # whose RBS is missing, rather than the project class between them.
141
+ def unresolvable_superclass_report(token)
142
+ name = bare_superclass_name(token)
143
+ root = token
144
+ seen = {}
145
+ while @source_classes.include?(name) && !seen[name]
146
+ seen[name] = true
147
+ nesting, superclass = @superclass_nestings[name]
148
+ return [:project, token] if superclass.nil?
149
+
150
+ root = resolve_superclass_token(superclass, nesting, @source_classes, name)
151
+ name = bare_superclass_name(root)
152
+ return [:project, token] if Reflection.rbs_class_known?(name, environment: @environment)
153
+ end
154
+ [:external, root]
117
155
  end
118
156
 
119
157
  # Lifts legacy plain-`Array[Type]` observation entries into {ObservedCall} carriers. Specs from the
@@ -128,13 +166,45 @@ module Rigor
128
166
  def run
129
167
  resolved = resolve_paths(@paths)
130
168
  @resolved_paths = resolved
169
+ # Issue #3035 — the scanned files Ruby loads only through a platform-conditional require; their
170
+ # candidates are demoted in {#demote_platform_guarded}.
171
+ @platform_guarded_files = PlatformGuardedFiles.call(
172
+ resolved, ruby_version: @configuration.target_ruby
173
+ )
131
174
  @environment = build_environment(resolved)
132
- candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
133
- annotate_effects(
134
- demote_overridden_base_methods(
135
- demote_unresolvable_superclasses(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
175
+ # ADR-84 WD2, as `Runner#run_analysis` mints it for `check` (#2300): one return-memo bucket for the whole
176
+ # run. Without it the memo keys on each file's merged `discovered_def_nodes`, a new table per file, so a
177
+ # callee reached from two files was re-inferred in each. A fresh token per run keeps a second run in one
178
+ # process from reading the first's entries, and the bucket is released with the other run-scoped slots
179
+ # when the run ends ({RunScopedSlots}), so a finished run holds none of it.
180
+ @run_generation = Object.new.freeze
181
+ # Both read the files as they are now, so a second `run` of one generator re-reads them.
182
+ @project_tables = nil
183
+ @file_base_scope = nil
184
+ # #2461 — which `sig/` targets the writer will write to, read from the files as they are now.
185
+ @writable_targets = {}
186
+ # ADR-130 WD5 — with interface dispatch off, so a return read through a `Rigor::Plugin` interface member
187
+ # (`_FactStore#publish: ... -> nil`) is not written into RBS a plugin author never wrote against the
188
+ # interface: the output is the output of erasure.
189
+ candidates = Inference::MethodDispatcher::RbsDispatch.without_interface_dispatch do
190
+ resolved.flat_map { |path| analyse_file(path, @environment) }
191
+ end
192
+ demoted = shelter_withheld_classes(
193
+ demote_platform_guarded(
194
+ demote_overridden_base_methods(
195
+ demote_bot_proposals(
196
+ demote_unresolvable_superclasses(
197
+ demote_generic_reopenings(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
198
+ )
199
+ )
200
+ )
136
201
  )
137
202
  )
203
+ # Again after the override demotion: a demoted method can leave its file with nothing to write, and a class
204
+ # shell riding on that file then declares nothing a subclass elsewhere could rest on.
205
+ annotate_effects(stamp_generic_reopenings(demote_unresolvable_superclasses(demoted)))
206
+ ensure
207
+ RunScopedSlots.release
138
208
  end
139
209
 
140
210
  private
@@ -209,10 +279,10 @@ module Rigor
209
279
  decls.each do |decl|
210
280
  next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
211
281
 
212
- name = (prefix + [decl.name.to_s.delete_prefix("::")]).join("::")
213
- return decl if name == class_name
282
+ path = ::Rigor::Environment::RbsLoader.declared_class_path(decl, prefix)
283
+ return decl if path.join("::") == class_name
214
284
 
215
- found = find_declaration(decl.members, name.split("::"), class_name)
285
+ found = find_declaration(decl.members, path, class_name)
216
286
  return found if found
217
287
  end
218
288
  nil
@@ -223,6 +293,88 @@ module Rigor
223
293
  (1..segments.size).any? { |n| generic.key?(segments.first(n).join("::")) }
224
294
  end
225
295
 
296
+ # Issue #2436 — a class the loaded RBS declares generic (`Array`, `Hash`, a gem's or the project's own) is
297
+ # reopened with that declaration's type parameters copied onto the header ({#stamp_generic_reopenings}),
298
+ # which is exact only while every parameter is plain. A bound or a default is a type spelled relative to the
299
+ # declaring file's namespace, and copying it under another nesting can resolve it to a different class, so
300
+ # rbs would reject the header as it rejects a parameterless one (`GenericParameterMismatchError`): the class,
301
+ # and every class whose signature mentions it, fails its definition build. A method of such a class, or of
302
+ # one nested in it, is skipped instead — unless `sig/` already declares the class, whose own header the
303
+ # writer then writes into ({#demote_inline_generic_classes}' rule).
304
+ def demote_generic_reopenings(candidates)
305
+ loader = @environment&.rbs_loader
306
+ return candidates if loader.nil?
307
+
308
+ layout = nil
309
+ candidates.map do |candidate|
310
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
311
+ next candidate unless namespace_prefixes(candidate.class_name).any? { |name| bounded_generic?(loader, name) }
312
+
313
+ layout ||= LayoutIndex.new(signature_paths: @configuration.signature_paths)
314
+ next candidate if layout.file_for(candidate.class_name)
315
+
316
+ demoted_candidate(candidate, :generic_reopening)
317
+ end
318
+ end
319
+
320
+ # The other half of {#demote_generic_reopenings}: every emittable candidate carries the header spelling of
321
+ # each generic class the run may open a header for — its own class, the classes it is nested in, and the
322
+ # file's class shells and theirs — so the writer and `--print` spell the same header. Method bodies keep
323
+ # `untyped` for element types; nothing here names `E` in a member.
324
+ #
325
+ # Issue #2580 — a skipped candidate is stamped too, and its class counts towards the map: the writer
326
+ # repairs a parameterless header an earlier run wrote into the file a candidate routes to, and a file whose
327
+ # every proposal is already declared (or untyped) would otherwise keep a header rbs rejects.
328
+ def stamp_generic_reopenings(candidates)
329
+ loader = @environment&.rbs_loader
330
+ return candidates if loader.nil?
331
+
332
+ spelled = header_type_params(candidates.flat_map { |c| [c.class_name, *c.class_shells] }.compact)
333
+ return candidates if spelled.empty?
334
+
335
+ candidates.map { |candidate| candidate.with_class_type_params(spelled) }
336
+ end
337
+
338
+ # #2603 — the header spelling {#header_type_params} gives `class_name` alone, or nil when the loaded RBS does
339
+ # not declare it generic with plain parameters only. The writer asks it for a bare header in a `sig/` file no
340
+ # candidate of the run routes to. Nil before {#run} has built the environment.
341
+ def generic_header_spelling(class_name)
342
+ loader = @environment&.rbs_loader
343
+ return nil if loader.nil?
344
+
345
+ params = loader.class_declaration_type_params(class_name)
346
+ "[#{params.join(', ')}]" if !params.empty? && params.all? { |p| plain_type_param?(p) }
347
+ end
348
+ public :generic_header_spelling
349
+
350
+ # `{ class name => "[unchecked out E]" }` for every name in `names`, or a namespace it is nested in, whose
351
+ # loaded declaration is generic with plain parameters only.
352
+ def header_type_params(names)
353
+ names.uniq.flat_map { |name| namespace_prefixes(name) }.uniq.each_with_object({}) do |name, acc|
354
+ spelling = generic_header_spelling(name)
355
+ acc[name] = spelling if spelling
356
+ end.freeze
357
+ end
358
+
359
+ def namespace_prefixes(class_name)
360
+ return [] if class_name.nil?
361
+
362
+ segments = class_name.split("::")
363
+ (1..segments.size).map { |n| segments.first(n).join("::") }
364
+ end
365
+
366
+ def bounded_generic?(loader, name)
367
+ params = loader.class_declaration_type_params(name)
368
+ !params.empty? && !params.all? { |param| plain_type_param?(param) }
369
+ end
370
+
371
+ # A name, a variance and `unchecked`, and nothing that spells a type: no upper or lower bound, no default.
372
+ def plain_type_param?(param)
373
+ %i[upper_bound lower_bound default_type].none? do |reader|
374
+ param.respond_to?(reader) && param.public_send(reader)
375
+ end
376
+ end
377
+
226
378
  # Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
227
379
  # override is not emitted itself.
228
380
  #
@@ -241,7 +393,7 @@ module Rigor
241
393
  # (#744 half 2) and is not decided here: this pass only stops sig-gen manufacturing the conflict.
242
394
  def demote_overridden_base_methods(candidates)
243
395
  superclasses = candidates.each_with_object({}) do |candidate, acc|
244
- (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup.sub(/\[.*\]\z/, "") }
396
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
245
397
  end
246
398
  return candidates if superclasses.empty?
247
399
 
@@ -256,6 +408,82 @@ module Rigor
256
408
  end
257
409
  end
258
410
 
411
+ # Issue #3029 — an emittable `-> bot` proposal for an overridden, abstract or template method is skipped
412
+ # ({BotProposalGuard}). It runs before {#demote_overridden_base_methods}, so a subclass's `bot` override this
413
+ # skips counts there as an override that is not emitted, and its parent's declaration is held back too.
414
+ def demote_bot_proposals(candidates)
415
+ guard = nil
416
+ candidates.map do |candidate|
417
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
418
+ next candidate if candidate.classification == Classification::INLINE_OVERWRITE
419
+ next candidate unless candidate.inferred_return.is_a?(Type::Bot)
420
+
421
+ guard ||= BotProposalGuard.new(project_tables)
422
+ reason = guard.reason_for(candidate)
423
+ reason ? demoted_candidate(candidate, reason) : candidate
424
+ end
425
+ end
426
+
427
+ # The reasons that withhold a member because of what its class's subclasses or callees do, not because the class
428
+ # itself cannot be declared. `:platform_guarded` counts too (#3035): a class whose every proposal the guard
429
+ # withheld — the raising-stub shape — still needs its shell on a subclass's file, or the subclass's own
430
+ # declaration reads `unresolvable-superclass` against a class this platform does load.
431
+ WITHHELD_FOR_OVERRIDES = %i[overridden_bot abstract_method template_method overridden_by_unsigned_subclass
432
+ platform_guarded].freeze
433
+ private_constant :WITHHELD_FOR_OVERRIDES
434
+
435
+ # Issue #3029 — a class whose members {#demote_bot_proposals} or {#demote_overridden_base_methods} withheld,
436
+ # leaving it nothing to emit, is still declared, as a class shell, so its subclasses keep their proposals.
437
+ # Without it an all-abstract base has nothing to write, {#demote_unresolvable_superclasses} finds no
438
+ # declaration for it, and every subclass proposal is skipped as `unresolvable-superclass`. The shell rides on
439
+ # each file that declares one of its subclasses, so it reaches disk wherever a subclass's members do. The writer
440
+ # spells its header with the run's recorded superclass and namespace kinds. A class the file already declares
441
+ # makes the shell a no-op, and one the loaded RBS declares gets none ({#wholly_withheld_classes}). A class whose
442
+ # own superclass chain is unresolvable is already skipped as `unresolvable-superclass` when this pass runs, so
443
+ # it is never withheld here and gets no shell.
444
+ def shelter_withheld_classes(candidates)
445
+ withheld = wholly_withheld_classes(candidates)
446
+ return candidates if withheld.empty?
447
+
448
+ shells_by_path = withheld_shell_paths(candidates, withheld)
449
+ candidates.map do |candidate|
450
+ extra = shells_by_path.fetch(candidate.path, nil)
451
+ next candidate if extra.nil?
452
+
453
+ shells = (candidate.class_shells + extra.to_a).uniq
454
+ shells.size == candidate.class_shells.size ? candidate : candidate.with_class_shells(shells)
455
+ end
456
+ end
457
+
458
+ # The classes to shelter: a withheld member and nothing to emit, and no declaration in the loaded RBS (`sig/`,
459
+ # the collection, gems). A declared class needs no shell for its subclasses' headers to resolve, and a shell
460
+ # spelling the source's superclass could contradict the declaration's (`RBS::SuperclassMismatchError`).
461
+ def wholly_withheld_classes(candidates)
462
+ by_class = candidates.select(&:class_name).group_by(&:class_name)
463
+ by_class.each_with_object(Set.new) do |(name, rows), acc|
464
+ next unless rows.any? { |c| WITHHELD_FOR_OVERRIDES.include?(c.skip_reason) }
465
+ next if rows.any? { |c| Classification::EMITTABLE.include?(c.classification) }
466
+ next if class_known?(name)
467
+
468
+ acc << name
469
+ end
470
+ end
471
+
472
+ # `{ path => Set[class name] }`: the files each withheld class's shell rides on, which are the files of every
473
+ # candidate whose class descends from it, however far down.
474
+ def withheld_shell_paths(candidates, withheld)
475
+ superclasses = candidates.each_with_object({}) do |candidate, acc|
476
+ (candidate.class_superclasses || {}).each { |name, sup| acc[name] = bare_superclass_name(sup) }
477
+ end
478
+ candidates.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |candidate, acc|
479
+ next if candidate.class_name.nil?
480
+
481
+ each_ancestor(candidate.class_name, superclasses) do |ancestor|
482
+ acc[candidate.path] << ancestor if withheld.include?(ancestor)
483
+ end
484
+ end
485
+ end
486
+
259
487
  # `[class name, method name]` pairs an UNSIGNED override shadows: for every candidate that will not
260
488
  # be emitted, every ancestor of its class paired with its method name.
261
489
  def unsigned_override_ancestors(candidates, superclasses)
@@ -287,6 +515,54 @@ module Rigor
287
515
  )
288
516
  end
289
517
 
518
+ # Issue #3035 — a file Ruby loads only under a `RUBY_ENGINE` / `RUBY_PLATFORM` / `defined?(…)`
519
+ # require guard is not an ordinary definition for the platform sig-gen cannot pick:
520
+ # `stackprof/truffleruby.rb`'s raising `StackProf.run` stub proposed `-> bot` against the C
521
+ # extension's real signature. {PlatformGuardedFiles} names those files; a candidate from one is
522
+ # demoted when another file also provides the method — the guarded proposal would compete with
523
+ # or duplicate the definition this platform actually loads — or when its inferred return is
524
+ # `bot`, the shape a raising stub takes and never a contract worth writing from a file the
525
+ # current engine may never read. It runs after every demotion — LAST of them — so an earlier
526
+ # pass still sees the guarded candidate as emittable: a platform-conditional override is no
527
+ # "unsigned subclass" ({#demote_overridden_base_methods}), {BotProposalGuard} still reads its
528
+ # `bot` on the discovery tables, and demoting the base for a file this platform never loads
529
+ # would withhold a declaration the guarded file cannot shadow here. It runs BEFORE
530
+ # {#shelter_withheld_classes}, which must see the final emittable set: a class left with
531
+ # nothing to emit only because this pass withheld its last member still needs its shell.
532
+ def demote_platform_guarded(candidates)
533
+ guarded = @platform_guarded_files
534
+ return candidates if guarded.nil? || guarded.empty?
535
+
536
+ providers = method_providers(candidates)
537
+ candidates.map do |candidate|
538
+ next candidate unless platform_guarded_demote?(candidate, guarded, providers)
539
+
540
+ demoted_candidate(candidate, :platform_guarded)
541
+ end
542
+ end
543
+
544
+ # `[class_name, kind, method_name]` → the expanded paths of every candidate providing it, the
545
+ # guarded file's own included — "another definition also provides" is the `!= own` test.
546
+ def method_providers(candidates)
547
+ candidates.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |candidate, acc|
548
+ next if candidate.class_name.nil?
549
+
550
+ acc[[candidate.class_name, candidate.kind, candidate.method_name]] <<
551
+ File.expand_path(candidate.path)
552
+ end
553
+ end
554
+
555
+ def platform_guarded_demote?(candidate, guarded, providers)
556
+ return false unless Classification::EMITTABLE.include?(candidate.classification)
557
+ return false if candidate.class_name.nil?
558
+
559
+ own = File.expand_path(candidate.path)
560
+ return false unless guarded.include?(own)
561
+
562
+ slot = [candidate.class_name, candidate.kind, candidate.method_name]
563
+ candidate.inferred_return&.erase_to_rbs == "bot" || providers[slot].any? { |path| path != own }
564
+ end
565
+
290
566
  # Issue #722 — resolves each recorded superclass token to the class Ruby means by it, and rewrites the
291
567
  # emitted spelling when the two differ.
292
568
  #
@@ -305,6 +581,10 @@ module Rigor
305
581
  def resolve_superclass_spellings(candidates)
306
582
  return candidates if @superclass_nestings.empty?
307
583
 
584
+ # Every shell counts here, written or not: the question is which class Ruby means by the token, and the
585
+ # source declares `Lib::Error` whether or not its file is written. Whether the resolved superclass reaches
586
+ # disk is {#demote_unresolvable_superclasses}'s question. Counting only written shells would leave `Error`
587
+ # unresolved, and a top-level `Error` some signature declares would then take it (#2454).
308
588
  known = candidates.each_with_object(Set.new) do |candidate, acc|
309
589
  acc << candidate.class_name if candidate.class_name
310
590
  acc.merge(candidate.class_shells || [])
@@ -324,20 +604,32 @@ module Rigor
324
604
  @superclass_nestings.each_with_object({}) do |(full, (nesting, token)), acc|
325
605
  next if token.nil? || nesting.empty?
326
606
 
327
- resolved = resolve_superclass_token(token, nesting, known)
607
+ resolved = resolve_superclass_token(token, nesting, known, full)
328
608
  acc[full] = resolved if resolved && resolved != token
329
609
  end
330
610
  end
331
611
 
332
612
  # The first candidate spelling that names a class this run declares or the RBS environment knows, or
333
- # nil when none does — an unresolvable superclass is #735's business, not this pass's.
334
- def resolve_superclass_token(token, nesting, known)
613
+ # the token itself when none does — an unresolvable superclass is #735's business, not this pass's.
614
+ #
615
+ # `full` is the class whose header carries the token. Ruby evaluates the superclass expression before the
616
+ # new constant exists, so the innermost candidate `full` itself (`module A; module C; class B < B`, where
617
+ # `A::C::B` is the class being opened) is not yet a candidate: the lookup falls through to `A::B` (#2579).
618
+ def resolve_superclass_token(token, nesting, known, full)
619
+ # A rooted token (`::Hash`) already names one class; the nesting must not capture it (#2459).
620
+ return token if token.start_with?("::")
621
+
335
622
  base = token.sub(/\[.*\]\z/, "")
336
- args = token[base.length..] || ""
337
623
  nesting.length.downto(1) do |i|
338
624
  candidate = "#{nesting[0, i].join('::')}::#{base}"
339
- return "#{candidate}#{args}" if known.include?(candidate) ||
340
- Reflection.rbs_class_known?(candidate, environment: @environment)
625
+ next if candidate == full
626
+
627
+ # The arguments {#record_superclass} computed belong to the top-level class of the bare name, so they are
628
+ # computed again for the class the token resolves to: `Hash` inside a `module Foo` declaring a plain
629
+ # `Foo::Hash` takes none (#2459).
630
+ return generic_superclass_spelling(candidate) if known.include?(candidate) ||
631
+ Reflection.rbs_class_known?(candidate,
632
+ environment: @environment)
341
633
  end
342
634
  token
343
635
  end
@@ -370,28 +662,96 @@ module Rigor
370
662
  # ran — source discovery and the plugins still type it. Emitting the class with the superclass merely
371
663
  # OMITTED would be the harmful middle: the class becomes RBS-known with only the emitted methods, so
372
664
  # every inherited call fires `call.undefined-method`.
665
+ #
666
+ # Demoting a class can leave a file with nothing to write, which takes the class shells riding on it off the
667
+ # emitted set, so the pass repeats until no further class becomes unresolvable. The emitted set only shrinks,
668
+ # so the unresolvable set only grows and the loop ends.
373
669
  def demote_unresolvable_superclasses(candidates)
374
670
  superclasses = candidates.each_with_object({}) do |candidate, acc|
375
671
  (candidate.class_superclasses || {}).each { |name, sup| acc[name] = sup }
376
672
  end
377
- # Everything this run declares, superclass or not: a chain that reaches `class Record` — emitted
378
- # here, with no superclass of its own — terminates in a class the next run WILL know.
379
- emitted = candidates.each_with_object(Set.new) do |candidate, acc|
380
- acc << candidate.class_name if candidate.class_name
381
- acc.merge(candidate.class_shells || [])
673
+ current = candidates
674
+ unresolvable = {}
675
+ loop do
676
+ found = unresolvable_superclass_roots(superclasses, declared_by_run(current))
677
+ return current if found.size == unresolvable.size
678
+
679
+ unresolvable = found
680
+ current = candidates.map do |candidate|
681
+ owner = unresolvable_for(candidate.class_name, unresolvable)
682
+ candidate = record_unresolvable_superclass(candidate, owner) if owner
683
+ drop_unresolvable_shells(candidate, unresolvable)
684
+ end
685
+ end
686
+ end
687
+
688
+ # Issue #2715 — a nested declaration's class shell rides on a surviving sibling's candidate (`class QueryColumn`
689
+ # beside `class Query < BaseRecord` wrapping `class QueryError`), so demoting the container's own candidates
690
+ # leaves the shell, and the writer renders the container around it with the unresolvable superclass: the next
691
+ # `check` fails `RBS::NoSuperclassFoundError`. A shell inside a demoted class goes with it, like a demoted
692
+ # class's own members, and a demoted candidate sheds them too: a shell left on one would still count as emitted
693
+ # ({#declared_by_run}), so a written subclass of the dropped nested class would fail its build.
694
+ # Writing the container bare instead would make it RBS-known without its ancestry, which
695
+ # a later run (or a path-subset run) would take for a resolvable superclass of its subclasses.
696
+ def drop_unresolvable_shells(candidate, unresolvable)
697
+ shells = (candidate.class_shells || []).to_a
698
+ kept = shells.reject do |name|
699
+ owner = unresolvable_for(name, unresolvable)
700
+ @unresolvable_superclasses[owner.first] ||= owner.last if owner
701
+ owner
382
702
  end
383
- unresolvable = superclasses.each_with_object({}) do |(name, _sup), acc|
703
+ kept.size == shells.size ? candidate : candidate.with_class_shells(kept)
704
+ end
705
+
706
+ def unresolvable_superclass_roots(superclasses, emitted)
707
+ superclasses.each_with_object({}) do |(name, _sup), acc|
384
708
  root = unresolved_superclass_root(name, superclasses, emitted)
385
709
  acc[name] = root if root
386
710
  end
387
- return candidates if unresolvable.empty?
388
-
389
- candidates.map do |candidate|
390
- owner = unresolvable_for(candidate.class_name, unresolvable)
391
- next candidate if owner.nil?
711
+ end
392
712
 
393
- record_unresolvable_superclass(candidate, owner)
713
+ # Everything this run puts on disk, superclass or not: a chain that reaches `class Record` — emitted here, with
714
+ # no superclass of its own — terminates in a class the next run WILL know. Only what the writer is sure to
715
+ # write counts, because a subclass written against a class nothing declares fails its build
716
+ # (`RBS::NoSuperclassFoundError`) on the next run:
717
+ #
718
+ # - a class with a candidate the writer is sure to write ({#written?}). A class whose every method is skipped
719
+ # is not written, so it does not count (#2437); a class shell on it would close it instead (#2438);
720
+ # - a class shell on a file with such a candidate: a shell on a file whose every method is skipped, already
721
+ # declared by `sig/`, or a tighter return the writer will not apply never reaches disk (#2454);
722
+ # - the wrapper namespaces of either, which the writer renders around them with their recorded superclasses.
723
+ #
724
+ # Counting too few only demotes a subclass, the output before #2454 (ADR-5).
725
+ def declared_by_run(candidates)
726
+ written = candidates.select { |candidate| written?(candidate) }
727
+ written_paths = written.to_set(&:path)
728
+ declared = Set.new
729
+ written.each { |candidate| declared << candidate.class_name if candidate.class_name }
730
+ candidates.each do |candidate|
731
+ declared.merge(candidate.class_shells || []) if written_paths.include?(candidate.path)
394
732
  end
733
+ declared.flat_map { |name| wrapper_names(name) }.to_set | declared
734
+ end
735
+
736
+ # `A::B::C` is written inside `A` and `A::B`.
737
+ def wrapper_names(name)
738
+ segments = name.split("::")
739
+ (1...segments.size).map { |n| segments.first(n).join("::") }
740
+ end
741
+
742
+ # Whether the writer will put `candidate` on disk: a candidate it is sure to write ({Writer.writes?}), and,
743
+ # when the run writes or compares files (`--write`, `--check`), aimed at a target that writer will write to
744
+ # (#2461). Print, diff and JSON runs have no writer: where a file would go depends on the working directory
745
+ # the writer takes its sig root from, and their output must not.
746
+ def written?(candidate)
747
+ return false unless Writer.writes?(candidate)
748
+ return true if @target_writer.nil?
749
+
750
+ target = @target_writer.path_mapper.target_for(candidate.path, class_name: candidate.class_name,
751
+ method_name: candidate.method_name,
752
+ kind: candidate.rbs_kind)
753
+ @writable_targets[target] = @target_writer.accepts_target?(target) unless @writable_targets.key?(target)
754
+ @writable_targets[target]
395
755
  end
396
756
 
397
757
  # A nested class or module goes with its wrapper. `WikiPage::Webhookable`'s declaration is emitted
@@ -417,9 +777,9 @@ module Rigor
417
777
  return superclass if seen[name]
418
778
 
419
779
  seen[name] = true
420
- # The recorded spelling may carry the type arguments {#generic_superclass_spelling} added; the
421
- # resolvability question is about the class, so it is asked of the bare name.
422
- base = superclass.sub(/\[.*\]\z/, "")
780
+ # The recorded spelling may carry the type arguments {#generic_superclass_spelling} added, and a rooted
781
+ # token its `::` (#2459); the resolvability question is about the class, so it is asked of the bare name.
782
+ base = bare_superclass_name(superclass)
423
783
  return nil if Reflection.rbs_class_known?(base, environment: @environment)
424
784
  return superclass unless emitted.include?(base)
425
785
 
@@ -460,30 +820,56 @@ module Rigor
460
820
  end.uniq
461
821
  end
462
822
 
463
- # #1154 — the project's discovery tables, read only by the declared-parameter ivar credit: it must see the ivar
464
- # writers in other files (an ancestor, another object's `instance_variable_set`, a reopening) as `rigor check`
465
- # does. Built once per run over the configured project paths as well as the files sig-gen reads, so a run over
466
- # one file declines what a `check` of it declines; every other part of the generator keeps its per-file scope.
467
- def ivar_writer_scope(base_scope)
468
- @ivar_writer_tables ||= Inference::ProjectSeedTables.for_paths(
823
+ # #1154 / #2300 — the project's discovery tables, built once per run over the configured project paths as well
824
+ # as the files sig-gen reads, so a run over one file reads what a `check` of it reads. Every file is indexed
825
+ # over them ({#file_base_scope}): a helper, an ancestor or a constant another file defines resolves as it does
826
+ # under `check` instead of reading ABSENT. The declared-parameter ivar credit reads them whole
827
+ # ({#ivar_writer_scope}).
828
+ def project_tables
829
+ @project_tables ||= Inference::ProjectSeedTables.for_paths(
469
830
  ((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
470
831
  )
471
- base_scope.with_discovery(base_scope.discovery.with(**@ivar_writer_tables))
832
+ end
833
+
834
+ # The member layouts stay per-file. sig-gen writes a value class for every `Data.define` / `Struct.new` layout
835
+ # the indexed scope carries ({#collect_meta_layouts}), so a seeded layout would re-emit every value class of the
836
+ # project in every file.
837
+ PER_FILE_TABLES = %i[data_member_layouts struct_member_layouts].freeze
838
+ private_constant :PER_FILE_TABLES
839
+
840
+ # #1154 — the declared-parameter ivar credit must see the ivar writers in other files (an ancestor, another
841
+ # object's `instance_variable_set`, a reopening) as `rigor check` does.
842
+ def ivar_writer_scope(base_scope)
843
+ base_scope.with_discovery(base_scope.discovery.with(**project_tables))
472
844
  end
473
845
 
474
846
  # #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
847
+ # #2489 — `base_scope` names the file, so the class-ivar census joins the writes another file's reopening and
848
+ # the class's ancestors make ({Inference::ScopeIndexer::IvarContributions}), as `rigor check`'s does.
475
849
  def index_file(root, base_scope)
476
850
  Inference::ScopeIndexer.index(
477
851
  root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
478
852
  )
479
853
  end
480
854
 
855
+ # The scope each file is indexed under: the project's tables less the per-file ones ({#project_tables}), and the
856
+ # run's return-memo token (see {#run}), which {#ivar_writer_scope} inherits, so every reading of a callee shares
857
+ # one memo bucket. Built once per run.
858
+ def file_base_scope(environment)
859
+ @file_base_scope ||= begin
860
+ base = Scope.empty(environment: environment)
861
+ tables = project_tables.except(*PER_FILE_TABLES)
862
+ tables = tables.merge(run_generation: @run_generation) if @run_generation
863
+ base.with_discovery(base.discovery.with(**tables))
864
+ end
865
+ end
866
+
481
867
  def analyse_file(path, environment)
482
868
  source = File.read(path)
483
869
  parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
484
870
  return [] if parse_result.errors.any?
485
871
 
486
- scope_index = index_file(parse_result.value, Scope.empty(environment: environment))
872
+ scope_index = index_file(parse_result.value, file_base_scope(environment).with_source_path(path))
487
873
 
488
874
  reset_file_maps
489
875
  @meta_layouts = collect_meta_layouts(scope_index)
@@ -518,11 +904,12 @@ module Rigor
518
904
  #
519
905
  # Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
520
906
  # `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
521
- # them). One under a condition, in a block or a lambda, passed to a call (`private def x`), or after an
522
- # uncertain `module_function` reset may not run, as far as this reading tells. A slot such a definer could
907
+ # them), or a `def` passed to `private`, `public` or `protected` in such a statement ({#walk_statements}, #2254).
908
+ # One under a condition, in a block or a lambda, or after an uncertain `module_function` reset may not run, as
909
+ # far as this reading tells. A slot such a definer could
523
910
  # still take over from the last certain one is contested, and nothing is written for it.
524
911
  #
525
- # @return `{ [class_name, kind, method_name] => [[offset, certain], ...] }`.
912
+ # @return the definers of each slot as `[offset, certain]` pairs, keyed by `[class_name, kind, method_name]`.
526
913
  MEMBER_OFFSET = -1
527
914
  private_constant :MEMBER_OFFSET
528
915
 
@@ -635,6 +1022,8 @@ module Rigor
635
1022
  @in_effect_refinements = nil
636
1023
  out = []
637
1024
  @block_depth = 0
1025
+ @hook_blocks = {}.compare_by_identity
1026
+ @hook_block_prefixes = []
638
1027
  @certain_bodies = Set.new.compare_by_identity
639
1028
  @certain_definers = Set.new.compare_by_identity
640
1029
  mark_certain_body(root.statements)
@@ -649,7 +1038,9 @@ module Rigor
649
1038
  when Prism::ClassNode, Prism::ModuleNode
650
1039
  return if descend_into_namespace?(node, prefix, out)
651
1040
  when Prism::SingletonClassNode
652
- if node.expression.is_a?(Prism::SelfNode) && node.body
1041
+ # `class << M` inside `module M` opens the same singleton as `class << self` (#3155).
1042
+ if node.body && (node.expression.is_a?(Prism::SelfNode) ||
1043
+ Inference::ScopeIndexer.def_receiver_targets_lexical_self?(node.expression, prefix))
653
1044
  mark_certain_body(node.body)
654
1045
  walk_defs(node.body, prefix, true, false, out)
655
1046
  return
@@ -677,16 +1068,37 @@ module Rigor
677
1068
  # the `self` one returns above): it defines a singleton method on `obj`, not a private `Object` method.
678
1069
  def walk_def_children(node, prefix, in_singleton_class, module_function_active, out)
679
1070
  return if refine_block_call?(node)
1071
+ # #2259 — a `def` in `class_methods do … end` (ActiveSupport::Concern) defines a method of the including class's
1072
+ # singleton, not of this module. Omitted, not written class-side: no precedent places it on the singleton.
1073
+ return if class_methods_block?(node)
680
1074
 
681
1075
  in_block = node.is_a?(Prism::BlockNode) || node.is_a?(Prism::LambdaNode) ||
682
1076
  node.is_a?(Prism::SingletonClassNode)
683
1077
  @block_depth += 1 if in_block
1078
+ hook_block = @hook_blocks.key?(node)
1079
+ @hook_block_prefixes.push(prefix) if hook_block
684
1080
  node.rigor_each_child do |child|
685
1081
  walk_defs(child, prefix, in_singleton_class, module_function_active, out)
686
1082
  end
1083
+ @hook_block_prefixes.pop if hook_block
687
1084
  @block_depth -= 1 if in_block
688
1085
  end
689
1086
 
1087
+ # Issue #3015 — the `included do … end` / `prepended do … end` statements of a body that runs `extend
1088
+ # ActiveSupport::Concern`: Ruby runs the block in the includer's class body, so a `def self.x` or `class << self`
1089
+ # def there is the includer's singleton method, not the module's, and {#collect_def_node} leaves it out.
1090
+ def mark_hook_blocks(namespace_node, prefix)
1091
+ body = namespace_node.body
1092
+ return unless namespace_node.is_a?(Prism::ModuleNode) && Inference::ScopeIndexer.concern_body?(body, prefix)
1093
+
1094
+ body.body.each do |statement|
1095
+ next unless statement.is_a?(Prism::CallNode) && statement.receiver.nil? &&
1096
+ %i[included prepended].include?(statement.name) && statement.block.is_a?(Prism::BlockNode)
1097
+
1098
+ @hook_blocks[statement] = true
1099
+ end
1100
+ end
1101
+
690
1102
  # [#722](https://github.com/rigortype/rigor/issues/722) residue 3 — the qualified name is built by
691
1103
  # `Source::ConstantPath.declaration_prefix`, the same reader the engine's declaration walks use, so a
692
1104
  # ROOTED header re-anchors here too. Appending the rendered name to the enclosing prefix wrote
@@ -706,6 +1118,7 @@ module Rigor
706
1118
 
707
1119
  full = child_prefix.join("::")
708
1120
  @namespace_kinds[full] = node.is_a?(Prism::ClassNode) ? :class : :module
1121
+ @source_classes << full if node.is_a?(Prism::ClassNode)
709
1122
  record_superclass(node, full, prefix)
710
1123
  walk_namespace_body(node, child_prefix, out)
711
1124
  true
@@ -725,6 +1138,17 @@ module Rigor
725
1138
  !@in_effect_refinements.class_body_refine?(node)
726
1139
  end
727
1140
 
1141
+ # A block call whose members are not this class body's: a `refine` block (#1763), and a `class_methods` block
1142
+ # (#2259), whose members belong to the including class's singleton.
1143
+ def skipped_block_call?(node)
1144
+ refine_block_call?(node) || class_methods_block?(node)
1145
+ end
1146
+
1147
+ def class_methods_block?(node)
1148
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :class_methods &&
1149
+ node.block.is_a?(Prism::BlockNode)
1150
+ end
1151
+
728
1152
  def declined_self_header?(node)
729
1153
  path = node.constant_path
730
1154
  path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
@@ -750,10 +1174,43 @@ module Rigor
750
1174
  # the name it resolves to may be a class another file declares. Resolved once every file has been
751
1175
  # walked, in {#resolve_superclass_spellings}.
752
1176
  spelling = generic_superclass_spelling(superclass)
1177
+ # Issue #2579 — a token that names the class being opened (`class A::B < A::B`, or a top-level `class B < B`
1178
+ # with nothing else of that name) would write a self-superclass, which RBS rejects as a recursive ancestor.
1179
+ # No superclass is safer than that, so the declaration is written without one.
1180
+ return if bare_superclass_name(spelling) == full
1181
+
753
1182
  # The token rides along because `@class_superclasses` is per-FILE scratch, reset before the next
754
1183
  # file, while this table is whole-run.
755
1184
  @superclass_nestings[full] = [prefix.dup, spelling]
756
1185
  @class_superclasses[full] = spelling
1186
+ register_core_subclass_shell(full, prefix, superclass)
1187
+ end
1188
+
1189
+ # Issue #2224 — a subclass of a core or stdlib class got a declaration only when it had a method of its own to
1190
+ # emit, so after `class MyHash < Hash; end` a method returning one was written `-> MyHash` against a class no
1191
+ # signature declared, and `rbs validate` failed with `NoTypeFoundError`. As a class shell it is declared
1192
+ # `class MyHash < ::Hash[untyped, untyped]` beside the file's other declarations. A class the RBS environment
1193
+ # already declares (the project's `sig/`, an earlier run) is left to that declaration, and a superclass the
1194
+ # header's nesting shadows is not core's: `Hash` inside a `module Foo` that declares `Foo::Hash` is the project's.
1195
+ # A rooted `::Hash` names core's class whatever the nesting declares, so the shadow cannot capture it (#2459).
1196
+ def register_core_subclass_shell(full, prefix, superclass)
1197
+ return if class_known?(full)
1198
+
1199
+ bare = bare_superclass_name(superclass)
1200
+ return unless @environment&.rbs_loader&.core_or_stdlib_class?(bare)
1201
+
1202
+ unless superclass.start_with?("::")
1203
+ namespaces = TypeNameQualification.enclosing_namespaces(prefix.join("::"))
1204
+ return if TypeNameQualification.shadowed?(bare.split("::").first, namespaces, method(:declared_constant?))
1205
+ end
1206
+
1207
+ @class_shells << full
1208
+ end
1209
+
1210
+ # The class a recorded superclass token names, as the fully qualified key every class-name set and the RBS
1211
+ # loader use: the type arguments {#generic_superclass_spelling} added and a rooted token's `::` both dropped.
1212
+ def bare_superclass_name(token)
1213
+ token.sub(/\[.*\]\z/, "").delete_prefix("::")
757
1214
  end
758
1215
 
759
1216
  # Issue #735 — a GENERIC superclass has to be written with its arguments. `class Entries < Array`
@@ -766,7 +1223,7 @@ module Rigor
766
1223
  loader = @environment&.rbs_loader
767
1224
  return superclass if loader.nil?
768
1225
 
769
- arity = loader.class_type_param_names(superclass).size
1226
+ arity = loader.class_type_param_names(bare_superclass_name(superclass)).size
770
1227
  return superclass if arity.zero?
771
1228
 
772
1229
  "#{superclass}[#{(['untyped'] * arity).join(', ')}]"
@@ -814,6 +1271,7 @@ module Rigor
814
1271
  @meta_layouts.each do |class_name, layout|
815
1272
  @namespace_kinds[class_name] = :class
816
1273
  @class_shells << class_name
1274
+ @source_classes << class_name
817
1275
  @class_superclasses[class_name] = MetaClassShape::SUPERCLASSES.fetch(layout.kind)
818
1276
  end
819
1277
  end
@@ -840,6 +1298,7 @@ module Rigor
840
1298
  return if namespace_node.body.nil?
841
1299
 
842
1300
  mark_certain_body(namespace_node.body)
1301
+ mark_hook_blocks(namespace_node, prefix)
843
1302
  walk_defs(namespace_node.body, prefix, false, false, out)
844
1303
  end
845
1304
 
@@ -862,6 +1321,8 @@ module Rigor
862
1321
  @certain_definers << stmt
863
1322
  # A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
864
1323
  mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
1324
+ # A `def` passed to a visibility call (`private def x`, #2254) runs with its statement.
1325
+ @certain_definers.merge(Inference::ModuleFunctionState.visibility_call_defs(stmt))
865
1326
  end
866
1327
  walk_defs(stmt, prefix, in_singleton_class, active, out)
867
1328
  end
@@ -876,7 +1337,12 @@ module Rigor
876
1337
  return
877
1338
  end
878
1339
 
879
- kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
1340
+ # `def Foo.x` inside `module Foo` is `def self.x` (#2950), and a def on another object's singleton is no method
1341
+ # of this class (#3155); the rule is the one check indexes defs by.
1342
+ kind = Inference::ScopeIndexer.def_side(node, prefix, in_singleton_class)
1343
+ return if kind.nil?
1344
+ return if kind == :singleton && @hook_block_prefixes.last == prefix
1345
+
880
1346
  class_name = prefix.join("::")
881
1347
  if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
882
1348
  @module_function_methods << node
@@ -959,8 +1425,8 @@ module Rigor
959
1425
  # `Object#initialize: () -> void`. The mismatch surfaces a `Ruby::MethodParameterMismatch` warning even
960
1426
  # when `rigor check` itself is clean.
961
1427
  #
962
- # Returning `nil` here causes `classify_def` to skip emission; returning `:emit_stub` causes
963
- # `initialize_stub_candidate` to emit a permissive `(<param shape>) -> void` stub matching the runtime
1428
+ # A default constructor is skipped (`classify_def` returns nil for it); a non-trivial one is not, and
1429
+ # {#initialize_stub_candidate} emits a permissive `(<param shape>) -> void` stub matching the runtime
964
1430
  # parameter list.
965
1431
  def initialize_excludes?(def_node, kind)
966
1432
  return false unless kind == :instance
@@ -1088,25 +1554,36 @@ module Rigor
1088
1554
  # return position it is not parenthesised.
1089
1555
  def positional_type(observations, index, prefix, owner = nil)
1090
1556
  types = observations.filter_map { |obs| obs.positional[index] }
1091
- "#{prefix}#{union_erase(types, owner: owner)}"
1557
+ "#{prefix}#{union_erase(widen_observed_params(types), owner: owner)}"
1092
1558
  end
1093
1559
 
1094
1560
  def render_keyword_param(keyword, observations, owner = nil)
1095
1561
  optional_marker = keyword.is_a?(Prism::OptionalKeywordParameterNode) ? "?" : ""
1096
1562
  types = observations.filter_map { |obs| obs.keyword[keyword.name] }
1097
- rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types, owner: owner))
1563
+ rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(widen_observed_params(types), owner: owner))
1098
1564
  "#{optional_marker}#{keyword.name}: #{rendered}"
1099
1565
  end
1100
1566
 
1567
+ # #2254 — an observed argument is one call's value, so a literal (`"hello"`) proposes its class (`String`), as
1568
+ # {#widen_value_literals} does for a return. Widening always: a parameter the body only compares against a
1569
+ # literal (a discriminant) is widened too, which loses precision and never adds a false positive.
1570
+ def widen_observed_params(types)
1571
+ types.map { |type| widen_value_literals(type) }
1572
+ end
1573
+
1574
+ # Issue #2459 — a ROOTED path (`::Hash`, `::A::B`) keeps its leading `::`: it names exactly one class, and the
1575
+ # passes after {#record_superclass} must not re-resolve it against the header's nesting, where a project
1576
+ # `Foo::Hash` would capture it. Every pass that compares the token against class names strips the anchor.
1101
1577
  def qualified_constant_path(constant_path)
1102
1578
  case constant_path
1103
1579
  when Prism::ConstantReadNode
1104
1580
  constant_path.name.to_s
1105
1581
  when Prism::ConstantPathNode
1106
- parent = qualified_constant_path(constant_path.parent) if constant_path.parent
1107
1582
  name = constant_path.name&.to_s
1108
1583
  return nil if name.nil?
1584
+ return "::#{name}" if constant_path.parent.nil?
1109
1585
 
1586
+ parent = qualified_constant_path(constant_path.parent)
1110
1587
  parent ? "#{parent}::#{name}" : name
1111
1588
  end
1112
1589
  end
@@ -1485,7 +1962,7 @@ module Rigor
1485
1962
  return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
1486
1963
  end
1487
1964
 
1488
- unless tighter?(declared, inferred) && !literal_decline?(inferred, def_node)
1965
+ unless tighter?(declared, inferred) && !literal_decline?(inferred, def_node, declared)
1489
1966
  return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
1490
1967
  end
1491
1968
 
@@ -1520,16 +1997,16 @@ module Rigor
1520
1997
  # about the return. The class's own declaration is kept whole instead: its visibility, the overload's
1521
1998
  # annotations, type parameters, parameters and block, with only the return replaced.
1522
1999
  #
1523
- # Three shapes are declined (`nil`):
2000
+ # Two shapes are declined (`nil`):
1524
2001
  # - several overloads. The body is typed once, so the inferred return covers every overload together and
1525
2002
  # says nothing about which overload returns what: collapsing them into one line would drop the
1526
2003
  # overloads, and giving each the joint return would widen the overloads the author wrote narrower.
1527
2004
  # - a name the class's own `sig/` spells as an `alias`. Its parameters are the aliased method's, and a
1528
2005
  # proposal would read as a rewrite of that method's line rather than of the alias. An alias only an
1529
2006
  # ancestor declares takes the override path below, like any other ancestor declaration.
1530
- # - nothing else: when only an ancestor declares the method, the proposal is a new override on this
1531
- # class, and is rendered from the `def`'s own shape like any new method. The ancestor's parameters
1532
- # describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
2007
+ # A method this class does not declare is not declined: when only an ancestor declares it, the proposal is a
2008
+ # new override on this class, rendered from the `def`'s own shape like any new method. The ancestor's
2009
+ # parameters describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
1533
2010
  def tighter_return_line(def_node, method_def, class_name, kind, inferred)
1534
2011
  member = signature_member(method_def, class_name)
1535
2012
  return render_rbs_line(def_node, inferred, class_name, kind) if member.nil?
@@ -1774,11 +2251,24 @@ module Rigor
1774
2251
 
1775
2252
  def structurally_covers?(inferred_member, declared_member)
1776
2253
  return true if inferred_member == declared_member
2254
+ return true if nominal_subclass_of?(inferred_member, declared_member)
1777
2255
 
1778
2256
  result = inferred_member.accepts(declared_member)
1779
2257
  result.respond_to?(:yes?) && result.yes?
1780
2258
  end
1781
2259
 
2260
+ # Issue #2914 — a proposed member that is a subclass of the declared one keeps that member: declared
2261
+ # `Prism::Node?` against an inferred `Prism::DefNode?` narrows what a caller receives without dropping a
2262
+ # branch the caller could observe, unlike a lost `nil` or a lost alternate. Only a bare nominal pair whose
2263
+ # ordering the analyzer's class data decides (`Environment#class_ordering`, RBS hierarchy included) is
2264
+ # read this way; a generic pair or an `:unknown` ordering stays uncovered, so the guard still declines.
2265
+ def nominal_subclass_of?(inferred_member, declared_member)
2266
+ return false unless inferred_member.is_a?(Type::Nominal) && declared_member.is_a?(Type::Nominal)
2267
+ return false unless inferred_member.type_args.empty? && declared_member.type_args.empty?
2268
+
2269
+ @environment&.class_ordering(inferred_member.class_name, declared_member.class_name) == :subclass
2270
+ end
2271
+
1782
2272
  GENERIC_COLLECTION_CLASSES = %w[
1783
2273
  Array Hash Set Range Enumerable Enumerator Enumerator::Lazy
1784
2274
  ].freeze
@@ -1794,8 +2284,8 @@ module Rigor
1794
2284
  # The two reasons a `Type::Constant` the body proves is not the method's contract. Both say the same
1795
2285
  # thing from opposite ends: the precision is real about this body and says nothing about the promise
1796
2286
  # the method makes to its callers.
1797
- def literal_decline?(inferred, def_node)
1798
- computed_literal_tightening?(inferred, def_node) || pins_literal_over_declaration?(inferred)
2287
+ def literal_decline?(inferred, def_node, declared)
2288
+ computed_literal_tightening?(inferred, def_node) || pins_literal_over_declaration?(inferred, declared)
1799
2289
  end
1800
2290
 
1801
2291
  # Issue #837 — a proposal that erases to an RBS LITERAL type never tightens an existing declaration.
@@ -1820,9 +2310,14 @@ module Rigor
1820
2310
  # is refused when ANY member is a literal: a mixed `("a" | Float)` pins `"a"` just as a bare one does.
1821
2311
  # A literal nested inside a shape carrier (`[1, 2]`, a `HashShape` value) is a different question,
1822
2312
  # already answered for the collection declarations that matter by {#narrows_collection_to_shape?}.
1823
- def pins_literal_over_declaration?(inferred)
2313
+ #
2314
+ # A literal the declaration itself spells is not pinned over it (#2914): declared `Prism::Node?` against
2315
+ # a proposed `Prism::DefNode?` carries the same `nil` member the author wrote, so only the narrowed
2316
+ # nominal arm is new. A literal the declaration does not spell is refused as before.
2317
+ def pins_literal_over_declaration?(inferred, declared)
1824
2318
  members = inferred.is_a?(Type::Union) ? inferred.members : [inferred]
1825
- members.any? { |member| rbs_literal?(member) }
2319
+ declared_members = declared.is_a?(Type::Union) ? declared.members : [declared]
2320
+ members.any? { |member| rbs_literal?(member) && !declared_members.include?(member) }
1826
2321
  end
1827
2322
 
1828
2323
  # `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
@@ -1912,8 +2407,35 @@ module Rigor
1912
2407
  # `owner:` is the class or module the rendered member belongs to. {AliasIndex} needs it to judge whether
1913
2408
  # a matching alias is one this reader would expect to see; a call site that cannot supply it renders the
1914
2409
  # type unfolded, which is the same output as before #1002.
2410
+ #
2411
+ # Issue #2230 — qualified last, after the alias fold has matched the unqualified spelling it keys on, so a core
2412
+ # name a namespace of the owner shadows (`Set` inside a `module Foo` that declares `Foo::Set`) is written `::Set`.
1915
2413
  def elaborated_rbs(type, owner: nil)
1916
- alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
2414
+ folded = alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
2415
+ captured = owner_type_param_names(owner)
2416
+ TypeNameQualification.qualify(folded, owner) { |name| captured.include?(name) || declared_constant?(name) }
2417
+ end
2418
+
2419
+ # Issue #2436 — inside `class Array[unchecked out E]` a bare `E` names the type variable, not a class `::E`,
2420
+ # so a class whose spelling equals one of the owner's declared type-parameter names is anchored. The names
2421
+ # come as `"Array::E"`, the form {TypeNameQualification.qualify} asks about for the owner's own namespace;
2422
+ # a namespace the owner is nested in does not bind its parameters inside the owner's declaration.
2423
+ def owner_type_param_names(owner)
2424
+ loader = owner && @environment&.rbs_loader
2425
+ return Set.new if loader.nil?
2426
+
2427
+ @owner_type_param_names ||= {}
2428
+ @owner_type_param_names.fetch(owner) do
2429
+ @owner_type_param_names[owner] = loader.class_declaration_type_params(owner).to_set do |p|
2430
+ "#{owner}::#{p.name}"
2431
+ end
2432
+ end
2433
+ end
2434
+
2435
+ # Whether a type name written as `name` resolves to a constant the run can see: one the RBS environment
2436
+ # declares, or a class or module the project's source declares, whether or not `sig/` has caught up with it.
2437
+ def declared_constant?(name)
2438
+ class_known?(name) || Reflection.discovered_class?(name, scope: file_base_scope(@environment))
1917
2439
  end
1918
2440
 
1919
2441
  # Issue #1002 — the reverse of {#translate_method_type_return}'s `alias_expander:`. Folding happens here,
@@ -2044,7 +2566,7 @@ module Rigor
2044
2566
  return
2045
2567
  end
2046
2568
  when Prism::CallNode
2047
- return if refine_block_call?(node)
2569
+ return if skipped_block_call?(node)
2048
2570
 
2049
2571
  collect_attr_call(node, prefix, in_singleton_class, ctx)
2050
2572
  end
@@ -2151,8 +2673,8 @@ module Rigor
2151
2673
  end
2152
2674
  end
2153
2675
 
2154
- # Build a { class_name => { attr_name_sym => Type } } map that records observation-derived types for
2155
- # ivars assigned directly from `def initialize` parameters. Only populated when `@observations` is
2676
+ # Records the observation-derived types of ivars assigned directly from `def initialize` parameters, keyed
2677
+ # by class name and then attribute name. Only populated when `@observations` is
2156
2678
  # non-empty (i.e. `--params=observed` was supplied). Matches the pattern `@ivar_name = param_name` where
2157
2679
  # `param_name` is a required / optional positional or keyword parameter of `initialize`.
2158
2680
  #
@@ -2285,7 +2807,7 @@ module Rigor
2285
2807
  node.rigor_each_child { |c| collect_init_ivar_obs(c, prefix, result) }
2286
2808
  end
2287
2809
 
2288
- # Derive { attr_name_sym => Type } for a single `def initialize` by matching `@ivar = param_name`
2810
+ # Derives the observation-derived attribute types of a single `def initialize`, matching `@ivar = param_name`
2289
2811
  # assignments against the available `[class_name, :initialize]` observations.
2290
2812
  def ivar_obs_from_initialize(class_name, def_node)
2291
2813
  obs_list = @observations[[class_name, :initialize]]
@@ -2300,8 +2822,8 @@ module Rigor
2300
2822
  build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
2301
2823
  end
2302
2824
 
2303
- # Map `{ ivar_name => param_name }` → `{ attr_name_sym => Type }` by looking up each param's observation
2304
- # types and unioning them.
2825
+ # Maps `{ ivar_name => param_name }` to attribute types by looking up each param's observation types and
2826
+ # unioning them.
2305
2827
  def build_ivar_obs_type_map(ivar_to_param, param_index, obs_list)
2306
2828
  ivar_to_param.filter_map do |ivar_name, param_name|
2307
2829
  types = collect_param_obs_types(obs_list, param_name, param_index[param_name])