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
@@ -12,11 +12,13 @@ require_relative "../type"
12
12
  require_relative "diagnostic"
13
13
  require_relative "dependency_recorder"
14
14
  require_relative "check_rules/rule_ids"
15
+ require_relative "check_rules/call_facts"
15
16
  require_relative "check_rules/inferred_param_guard"
16
17
  require_relative "check_rules/declaration_sourced_guard"
17
18
  require_relative "check_rules/published_constant_guard"
18
19
  require_relative "check_rules/dead_version_guard_arms"
19
20
  require_relative "check_rules/rule_walk"
21
+ require_relative "check_rules/always_raises_shield"
20
22
  require_relative "check_rules/always_truthy_condition_collector"
21
23
  require_relative "check_rules/unreachable_clause_collector"
22
24
  require_relative "check_rules/shadowed_rescue_collector"
@@ -33,6 +35,8 @@ require_relative "check_rules/rebinding_block_callees"
33
35
  require_relative "check_rules/rbs_receiver_surface"
34
36
  require_relative "check_rules/special_global_setters"
35
37
  require_relative "check_rules/ruby_deprecations"
38
+ require_relative "check_rules/plugin_api_surface"
39
+ require_relative "../typing_state"
36
40
 
37
41
  module Rigor
38
42
  module Analysis
@@ -53,7 +57,7 @@ module Rigor
53
57
  #
54
58
  # - implicit-self calls (no `node.receiver`) — too noisy
55
59
  # without per-method RBS for every helper in the class.
56
- # - dynamic / unknown receivers (`Dynamic[T]`, `Top`, `Union`)
60
+ # - dynamic / unknown receivers (`Dynamic[T]`, `Top`)
57
61
  # — by definition we cannot enumerate the method set.
58
62
  # - shape carriers: `Tuple` → "Array", `HashShape` → "Hash",
59
63
  # `Constant` → the constant's class — `concrete_class_name`
@@ -86,9 +90,8 @@ module Rigor
86
90
 
87
91
  module_function
88
92
 
89
- # Yields diagnostics for every unrecognised method call on
90
- # a typed receiver in `root`'s subtree. The caller MUST
91
- # have already produced `scope_index` through
93
+ # Returns the diagnostics for `root`'s subtree across the rule
94
+ # catalogue. The caller MUST have already produced `scope_index` through
92
95
  # `Rigor::Inference::ScopeIndexer.index(root, default_scope:)`.
93
96
  #
94
97
  # @param path — used to populate
@@ -110,7 +113,8 @@ module Rigor
110
113
  diagnostics = collectors[:main_pass].results.dup
111
114
  diagnostics.concat(self_undefined_method_diagnostics(path, self_call_misses, root, scope_index))
112
115
  COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
113
- diagnostics.concat(send(builder, path, collectors[role].results))
116
+ collector = OPTIONAL_COLLECTOR_ROLES.include?(role) ? collectors[role] : collectors.fetch(role)
117
+ diagnostics.concat(send(builder, path, collector.results)) if collector
114
118
  end
115
119
  # ADR-47 WD5 — the dead arm of a decidable version guard (#627) reports nothing: it cannot run on
116
120
  # the Ruby being checked with, so a diagnostic there is a false positive. Applied before
@@ -141,18 +145,25 @@ module Rigor
141
145
  ivar_writes: :ivar_write_mismatch_diagnostics,
142
146
  dead_assignments: :dead_assignment_diagnostics,
143
147
  duplicate_hash_keys: :duplicate_hash_key_diagnostics,
144
- return_in_ensure: :return_in_ensure_diagnostics
148
+ return_in_ensure: :return_in_ensure_diagnostics,
149
+ plugin_api_surface: :plugin_api_surface_diagnostics
145
150
  }.freeze
146
151
  private_constant :COLLECTOR_DIAGNOSTIC_BUILDERS
147
152
 
153
+ # The roles {.build_node_collectors} builds only on request: `plugin_api_surface`, when the rule is active
154
+ # ({PluginApiSurface.active?}). {.diagnose} skips an absent optional role; every other role is required, and a
155
+ # missing one raises.
156
+ OPTIONAL_COLLECTOR_ROLES = %i[plugin_api_surface].freeze
157
+
148
158
  # The verbatim per-node dispatch of the former inline main pass
149
159
  # (`diagnose`'s `Source::NodeWalker.each` `case`), now invoked by
150
160
  # {MainPassCollector} on the shared {RuleWalk}. Returns the
151
161
  # diagnostics for one node, in the same emission order as before.
152
- def main_pass_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
162
+ def main_pass_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil,
163
+ raise_shield = nil)
153
164
  case node
154
165
  when Prism::CallNode
155
- call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites)
166
+ call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
156
167
  when Prism::DefNode
157
168
  [
158
169
  refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites),
@@ -174,7 +185,8 @@ module Rigor
174
185
  # check in the body stands. The refine-body test runs only once a mismatch would report, so a file pays the
175
186
  # {LexicalMethodSites} walk only then.
176
187
  def refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites)
177
- diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
188
+ replaced = lexical_sites&.replaced_singleton_defs(scope_index)
189
+ diagnostic = return_type_mismatch_diagnostic(path, node, scope_index, replaced)
178
190
  return diagnostic if diagnostic.nil? || lexical_sites.nil?
179
191
 
180
192
  lexical_sites.refinements.refinement_def?(node) ? nil : diagnostic
@@ -187,11 +199,17 @@ module Rigor
187
199
  # traversal, and hand the populated set back to {.diagnose} as
188
200
  # `node_collectors:`. The main pass needs `path` because its per-node
189
201
  # diagnostics carry it (ADR-53 B3c hosts it on the same walk).
190
- def build_node_collectors(path, scope_index, root = nil)
202
+ #
203
+ # `plugin_api_surface:` adds the `plugin.api-surface` collector, which the runner asks for only when the rule
204
+ # resolves to a reporting severity ({PluginApiSurface.active?}).
205
+ def build_node_collectors(path, scope_index, root = nil, plugin_api_surface: false)
191
206
  eval_ranges = receiver_eval_block_ranges(root)
192
207
  lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
193
- main_pass = ->(node) { main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites) }
194
- {
208
+ raise_shield = root && AlwaysRaisesShield.new(root)
209
+ main_pass = lambda do |node|
210
+ main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
211
+ end
212
+ collectors = {
195
213
  main_pass: MainPassCollector.new(main_pass),
196
214
  void_value_use: VoidValueUseCollector.new(scope_index),
197
215
  always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
@@ -202,6 +220,8 @@ module Rigor
202
220
  duplicate_hash_keys: DuplicateHashKeyCollector.new(scope_index),
203
221
  return_in_ensure: ReturnInEnsureCollector.new(scope_index)
204
222
  }
223
+ collectors[:plugin_api_surface] = PluginApiSurface::Collector.new(scope_index) if plugin_api_surface
224
+ collectors
205
225
  end
206
226
 
207
227
  # A {RuleWalk::CollectorDriver} over a built-in collector set, for a
@@ -279,8 +299,11 @@ module Rigor
279
299
  diagnostics = []
280
300
  eval_ranges = receiver_eval_block_ranges(root)
281
301
  lexical_sites = LexicalMethodSites.new(root, stamped_refinements(scope_index, root))
302
+ raise_shield = AlwaysRaisesShield.new(root)
282
303
  Source::NodeWalker.each(root) do |node|
283
- diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites))
304
+ diagnostics.concat(
305
+ main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites, raise_shield)
306
+ )
284
307
  end
285
308
  diagnostics
286
309
  end
@@ -300,18 +323,22 @@ module Rigor
300
323
  shadow_verify_node_collectors(path, root, scope_index, collectors)
301
324
  end
302
325
 
303
- def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
326
+ # Issue #2786 — the rules that type the call's receiver read it from one {CallFacts}, so the receiver chain is
327
+ # typed once per call node rather than once per rule.
328
+ def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil,
329
+ raise_shield = nil)
330
+ facts = CallFacts.new(node, scope_index)
304
331
  [
305
- undefined_method_diagnostic(path, node, scope_index, lexical_sites),
332
+ undefined_method_diagnostic(path, node, facts, lexical_sites),
306
333
  unresolved_toplevel_diagnostic(path, node, scope_index, eval_ranges, lexical_sites),
307
- wrong_arity_diagnostic(path, node, scope_index, lexical_sites),
308
- argument_type_diagnostic(path, node, scope_index, lexical_sites),
309
- nil_receiver_diagnostic(path, node, scope_index, lexical_sites),
334
+ wrong_arity_diagnostic(path, node, facts, lexical_sites),
335
+ argument_type_diagnostic(path, node, facts, lexical_sites),
336
+ nil_receiver_diagnostic(path, node, facts, lexical_sites),
310
337
  dump_type_diagnostic(path, node, scope_index),
311
338
  assert_type_diagnostic(path, node, scope_index),
312
- always_raises_diagnostic(path, node, scope_index, lexical_sites),
339
+ always_raises_diagnostic(path, node, facts, lexical_sites, raise_shield),
313
340
  raise_non_exception_diagnostic(path, node, scope_index),
314
- visibility_mismatch_diagnostic(path, node, scope_index, lexical_sites)
341
+ visibility_mismatch_diagnostic(path, node, facts, lexical_sites)
315
342
  ].compact
316
343
  end
317
344
 
@@ -407,6 +434,11 @@ module Rigor
407
434
  end
408
435
  end
409
436
 
437
+ # ADR-128 WD6 / ADR-130 slice 2 — `plugin.api-surface`, from its collector's results.
438
+ def plugin_api_surface_diagnostics(path, results)
439
+ PluginApiSurface.diagnostics(path, results)
440
+ end
441
+
410
442
  # ADR-100 WD2 — `static.value-use.void`. Its value-context slot inspection rides the shared per-node
411
443
  # {RuleWalk} like the other collectors, so the file is not re-traversed for it. Each result is a
412
444
  # value-context use of a call whose author-declared `-> void` return the engine recovered to `top`.
@@ -740,17 +772,17 @@ module Rigor
740
772
  class << self
741
773
  private
742
774
 
743
- def undefined_method_diagnostic(path, call_node, scope_index, lexical_sites = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
775
+ def undefined_method_diagnostic(path, call_node, facts, lexical_sites = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
744
776
  return nil if call_node.receiver.nil?
745
777
 
746
- scope = scope_index[call_node]
778
+ scope = facts.scope
747
779
  return nil if scope.nil?
748
780
 
749
781
  # The exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
750
782
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
751
783
  # gate belongs where that one consult is rather than duplicated into each shape's path.
752
784
  #
753
- # The union side of that placement IS reachable, contrary to what this comment used to say. The
785
+ # The union side of that placement is reachable. The
754
786
  # shape needs a non-nil union of at least two RBS-DECLARED classes — an undeclared arm is an
755
787
  # ADR-26 open receiver and `union_arm_blocks_undefined_fire?` bails before the method lookup,
756
788
  # which is why the earlier probes read silent and were misread as the gate being unreachable.
@@ -776,7 +808,7 @@ module Rigor
776
808
  # below — preserving that keeps `&.` from newly firing on the
777
809
  # non-nil constituent (which, for a cross-file project def, would
778
810
  # be a working-code false positive).
779
- receiver_type = safe_navigation_receiver(call_node, scope)
811
+ receiver_type = safe_navigation_receiver(call_node, facts.receiver_type)
780
812
 
781
813
  # #320 — the private-singleton-object idiom (`class << Merger = Object.new`). The body's methods
782
814
  # are recorded on the constant's own name, but the receiver reads back as `Object`, so the
@@ -797,13 +829,6 @@ module Rigor
797
829
  return union_undefined_method_diagnostic(path, call_node, receiver_type, scope, lexical_sites)
798
830
  end
799
831
 
800
- # ADR-26 — a plugin may declare a class "open": one
801
- # known to respond beyond its RBS-declared method
802
- # surface (e.g. `ActiveRecord::Relation`, which
803
- # delegates an unbounded set of user-defined scopes to
804
- # its model). Flagging an undefined method on a class
805
- # with an open dynamic surface is unsound, so the rule
806
- # skips it.
807
832
  # An unbounded receiver surface: either a plugin-declared
808
833
  # open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
809
834
  # a type Rigor synthesized (a missing-namespace module / a
@@ -839,6 +864,7 @@ module Rigor
839
864
  return nil if method_def
840
865
 
841
866
  return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
867
+ return nil if open_project_singleton?(receiver_type, class_name, call_node, scope)
842
868
  return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
843
869
  call_node.receiver)
844
870
 
@@ -979,16 +1005,268 @@ module Rigor
979
1005
  call_node: call_node)
980
1006
  return true if module_mixin_receiver?(receiver_type, scope)
981
1007
  return true if mixin_self_class_receiver?(call_node, scope)
982
- return true if unknown_mixin_includer?(class_name, scope)
1008
+ return true if unknown_mixin_includer?(class_name, scope, kind)
983
1009
  return true if object_mixin_may_define?(receiver_type, class_name, call_node.name, scope)
984
1010
  return true if ancestry_declares_method?(scope, class_name, call_node.name, kind)
1011
+ return true if open_names_through_ancestors?(scope, class_name, kind)
1012
+ return true if foreign_singleton_through_ancestors?(scope, class_name, call_node.name, kind)
985
1013
 
986
1014
  rbs_ancestor_may_answer?(class_name, call_node.name, scope, kind)
987
1015
  end
988
1016
 
1017
+ # Issue #2793 — the class, or a project class or module on its chain, carries
1018
+ # `Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK`: a macro there defines methods under names its syntax
1019
+ # does not spell (`def_delegators :@x, *NAMES`), so no name is provably missing. A chain cut at its budget
1020
+ # never reaches here: {#ancestry_declares_method?} reads the same chain first and declines on the cut.
1021
+ # A non-literal `define_method` records its own side's twin of the mark instead, read beside it here.
1022
+ OPEN_NAMES_MARKS = {
1023
+ instance: [Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK,
1024
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_INSTANCE_NAMES_MARK].freeze,
1025
+ singleton: [Scope::DiscoveryIndex::ENVELOPE_OPEN_NAMES_MARK,
1026
+ Scope::DiscoveryIndex::ENVELOPE_OPEN_SINGLETON_NAMES_MARK].freeze
1027
+ }.freeze
1028
+ private_constant :OPEN_NAMES_MARKS
1029
+
1030
+ def open_names_through_ancestors?(scope, class_name, kind)
1031
+ side = kind == :singleton ? :singleton : :instance
1032
+ marks = OPEN_NAMES_MARKS.fetch(side)
1033
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, :methods)
1034
+ # A project class with no `def` of its own is held as an external entry (#2094), so an external's
1035
+ # candidate names are asked too.
1036
+ found = chain.search(scope) do |entry|
1037
+ names = entry.external? ? entry.candidates : [entry.name]
1038
+ names.any? do |name|
1039
+ bucket = scope.parameter_envelopes_of(name)
1040
+ marks.any? { |mark| bucket.key?(mark) } ||
1041
+ (side == :instance && (computed_definer_called?(scope, name, bucket) ||
1042
+ hook_block_macro_defines?(scope, name)))
1043
+ end
1044
+ end
1045
+ !found.nil?
1046
+ end
1047
+
1048
+ # Issue #3018 — a class object's singleton method that `def Foo.x`, `Foo.define_singleton_method(:x)` or a
1049
+ # `Foo.singleton_class.class_eval` def defines outside `Foo`'s body records no `:singleton` row, only an
1050
+ # envelope entry. The class-object path reads the definer's own entry
1051
+ # (`Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND`) on the chain's class-object entries, the class and
1052
+ # its superclasses, so a subclass inherits it. An extended module's entry is instance-side: `extend M` adds
1053
+ # `M`'s instance methods, not a `def M.x`. It does not read the opaque `[:singleton, x]` entry beside it, which
1054
+ # a name-rewriting class-body macro (`my_attr :x`) files too without defining `x` on the class object.
1055
+ def foreign_singleton_through_ancestors?(scope, class_name, method_name, kind)
1056
+ return false unless kind == :singleton
1057
+
1058
+ key = [Scope::DiscoveryIndex::ENVELOPE_OBJECT_SINGLETON_KIND, method_name.to_sym]
1059
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
1060
+ found = chain.search(scope, side: :singleton) do |entry|
1061
+ names = entry.external? ? entry.candidates : [entry.name]
1062
+ names.any? { |name| scope.parameter_envelopes_of(name).key?(key) }
1063
+ end
1064
+ !found.nil?
1065
+ end
1066
+
1067
+ # Issue #2901 — the body of `name` calls, on `self`, a method its singleton chain resolves to a computed
1068
+ # definer (`Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND`): a class macro of its own, a
1069
+ # superclass's or an extended module's, which runs `define_method` under a name no table holds. The first
1070
+ # entry on that chain carrying the macro's definer entry answers; a closer override that defines nothing is
1071
+ # not looked for, so it leaves the class open.
1072
+ #
1073
+ # Issue #2975 — the definer entry carries its side: the class's own singleton entry answers with a singleton
1074
+ # method (`def self.my_attr`), an extended module's entry with an instance method, so an instance `def my_attr`
1075
+ # of the class itself opens nothing. A macro patched onto `Module` (or `Class`, `Object`, `Kernel`,
1076
+ # `BasicObject`) as an instance method is on every class object's singleton chain past its own classes.
1077
+ def computed_definer_called?(scope, name, bucket)
1078
+ macros = bucket.each_key.filter_map do |key|
1079
+ key.last if key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_BODY_CALL_KIND
1080
+ end
1081
+ return false if macros.empty?
1082
+
1083
+ chain = Scope::ResolutionChain.for(scope, name.to_s, :singleton, :methods)
1084
+ found = macros.any? do |macro|
1085
+ !chain.search(scope) { |entry| macro_entry_defines?(scope, entry, macro) }.nil? ||
1086
+ universal_macro_defines?(scope, macro)
1087
+ end
1088
+ found || hook_extended_macro_defines?(scope, name, macros)
1089
+ end
1090
+
1091
+ # Issue #2975 — a module that a class on `name`'s superclass chain includes or prepends directly, and whose
1092
+ # `included` / `prepended` hook extends its includer with a module (`def self.included(base) =
1093
+ # base.extend(ClassMethods)`), puts that module's instance methods on `name`'s singleton chain, which records no
1094
+ # edge for it. Ruby runs the hook for the direct includer only, so a module included through another module does
1095
+ # not count; the chain's levels flatten a class's transitive mixins, so each level class's own mixin list is
1096
+ # read, one hop, never followed further. Each mixin and each extended constant resolves to the first candidate
1097
+ # the project declares, the innermost cref first, whether or not RBS declares the modules; a concern's
1098
+ # `ClassMethods` resolves as `const_get` does instead (#2986).
1099
+ def hook_extended_macro_defines?(scope, name, macros)
1100
+ definers = macro_definers(macros)
1101
+ direct_hook_mixins(scope, name).any? { |mixin, _edges| hook_extension_defines?(scope, mixin, definers) }
1102
+ end
1103
+
1104
+ # Issue #2986 — an ActiveSupport::Concern's `included do … end` block runs in the class body of a class that
1105
+ # includes the concern directly, and its `prepended do … end` block in one that prepends it, so a macro call
1106
+ # in the block is read as that class's own, against the module the concern extends it with. A call in the
1107
+ # concern's own module body is no includer's: Ruby raises there as the module loads.
1108
+ #
1109
+ # Issue #2992 — Ruby runs the block in every direct includer, a module included, so the read is made for each
1110
+ # entry of the receiver's chain (`owner`) against that entry's own mixins, one hop. A concern included into a
1111
+ # concern after its `extend ActiveSupport::Concern` runs no block there: `Concern#append_features` records it as
1112
+ # a dependency, and the outer concern's final includer includes it, on that includer's own edge
1113
+ # ({#with_concern_dependencies}). Only a mixin whose one `include` / `prepend` statement is such a dependency
1114
+ # is left out of `owner`'s own read: one written ahead of the `extend`, or again in a reopening that may load
1115
+ # first, is an ordinary include there.
1116
+ def hook_block_macro_defines?(scope, owner)
1117
+ own_mixin_edges(scope, owner, deferred: true).any? do |mixin, edges|
1118
+ macros = scope.parameter_envelopes_of(mixin).each_key.filter_map do |key|
1119
+ key[2] if key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_HOOK_BODY_CALL_KIND &&
1120
+ edges.include?(Scope::ResolutionChain.effective_hook(scope.discovery, mixin, key[1]))
1121
+ end
1122
+ !macros.empty? && hook_extension_defines?(scope, mixin, macro_definers(macros))
1123
+ end
1124
+ end
1125
+
1126
+ def macro_definers(macros)
1127
+ macros.map { |macro| [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, :instance, macro] }
1128
+ end
1129
+
1130
+ # Each module a class on `name`'s superclass chain includes or prepends directly, resolved, with the hook
1131
+ # blocks its edges run: `:included` for an `include`, `:prepended` for a `prepend`; a concern's dependencies
1132
+ # follow it on the same edges.
1133
+ def direct_hook_mixins(scope, name)
1134
+ chain = Scope::ResolutionChain.for(scope, name.to_s, :instance, :methods)
1135
+ chain.record(scope) if Analysis::DependencyRecorder.active?
1136
+ chain.level_classes.compact.flat_map { |klass| own_mixin_edges(scope, klass) }
1137
+ end
1138
+
1139
+ # The modules `owner` itself includes or prepends, as {#direct_hook_mixins} gives them. With `deferred:`, a
1140
+ # provable dependency of a concern `owner` is left out ({#hook_block_macro_defines?}).
1141
+ def own_mixin_edges(scope, owner, deferred: false)
1142
+ mixins = scope.includes_of(owner)
1143
+ prepends = scope.prepends_of(owner)
1144
+ deferred_mixins = deferred ? deferred_dependencies(scope, owner, mixins) : NO_DEFERRED
1145
+ mixins.uniq.flat_map do |raw|
1146
+ mixin = declared_constant(scope, owner, raw)
1147
+ next [] unless mixin
1148
+
1149
+ edges = []
1150
+ edges << :prepended if prepends.include?(raw)
1151
+ edges << :included if mixins.count(raw) > prepends.count(raw)
1152
+ next [] if deferred_mixins.include?(raw)
1153
+
1154
+ with_concern_dependencies(scope, mixin).map { |member| [member, edges] }
1155
+ end
1156
+ end
1157
+
1158
+ NO_DEFERRED = [].freeze
1159
+ private_constant :NO_DEFERRED
1160
+
1161
+ # The spellings among `owner`'s mixins that name a concern dependency of `owner`, when `owner` is declared in
1162
+ # one file: a reopening may write the same `include` without the `extend`, and load first.
1163
+ def deferred_dependencies(scope, owner, mixins)
1164
+ sites = scope.discovery.discovered_class_sources[owner.to_s]
1165
+ return NO_DEFERRED if sites.nil? || sites.size > 1
1166
+
1167
+ resolver = Scope::ResolutionChain.resolver_for(scope, :methods)
1168
+ dependencies = Scope::ResolutionChain.concern_dependencies(scope.discovery, resolver, owner)
1169
+ return NO_DEFERRED if dependencies.empty?
1170
+
1171
+ Scope::ResolutionChain.dependency_spellings(scope.discovery, owner).select do |raw|
1172
+ mixins.include?(raw) && dependencies.include?(resolver.resolve_declared(owner, raw))
1173
+ end
1174
+ end
1175
+
1176
+ # Issue #2992 — `mixin` and, when it is a concern, the concerns it includes or prepends, at any depth through
1177
+ # concerns ({Scope::ResolutionChain.concern_dependencies}): `Concern#append_features` (or `prepend_features`)
1178
+ # includes (or prepends) each of them into the final includer ahead of `mixin` itself, on the includer's own
1179
+ # edge to `mixin`, whatever edge each was given. A reader files each one's sites as it reads its envelopes.
1180
+ def with_concern_dependencies(scope, mixin)
1181
+ resolver = Scope::ResolutionChain.resolver_for(scope, :methods)
1182
+ [mixin, *Scope::ResolutionChain.concern_dependencies(scope.discovery, resolver, mixin)]
1183
+ end
1184
+
1185
+ def hook_extension_defines?(scope, mixin, definers)
1186
+ hook_extends(scope, mixin).any? do |written, lookup|
1187
+ target = if lookup == :const_get then const_get_constant(scope, mixin, written)
1188
+ else declared_constant(scope, mixin, written)
1189
+ end
1190
+ record_hook_target_edges(scope, mixin, written, lookup) if Analysis::DependencyRecorder.active?
1191
+ target && definers.any? { |definer| scope.parameter_envelopes_of(target).key?(definer) }
1192
+ end
1193
+ end
1194
+
1195
+ # Issue #2992 — the read depends on the extended module's files whether or not it resolved: an empty
1196
+ # `module ClassMethods; end` is no project class yet, and the edit that gives it the definer makes it one. So
1197
+ # every name the lookup tried files its declaring sites, and the constant's last segment its negative class
1198
+ # edge, which a NEW file declaring it fires.
1199
+ def record_hook_target_edges(scope, mixin, written, lookup)
1200
+ candidates = if lookup == :const_get
1201
+ chain = Scope::ResolutionChain.for(scope, mixin, :instance, :methods)
1202
+ chain.entries.flat_map { |entry| entry.external? ? entry.candidates : [entry.name] }
1203
+ .map { |name| "#{name}::#{written}" } << written
1204
+ else
1205
+ own = written.start_with?("::") ? [] : ["#{mixin}::#{written}"]
1206
+ own + scope.ancestor_name_candidates(mixin, written)
1207
+ end
1208
+ candidates.each { |candidate| Scope::ResolutionChain.record_class(scope, candidate.delete_prefix("::")) }
1209
+ Analysis::DependencyRecorder.read_last_segment(:class, written)
1210
+ end
1211
+
1212
+ # The constant `raw`, written in `owner`'s body, as the first candidate the project declares: `Owner::Raw`
1213
+ # first, since `owner` is the innermost cref there, then the enclosing namespaces. Nil when none is declared.
1214
+ def declared_constant(scope, owner, raw)
1215
+ candidates = scope.ancestor_name_candidates(owner, raw)
1216
+ candidates = ["#{owner}::#{raw}", *candidates] unless raw.start_with?("::")
1217
+ candidates.find { |candidate| scope.known_user_class?(candidate) }
1218
+ end
1219
+
1220
+ # Issue #2986 — `owner.const_get(raw)`, as `Concern#append_features` looks `ClassMethods` up: `owner` and its
1221
+ # ancestors, then `Object`, never the namespaces enclosing `owner`. Issue #2997 — a `class_methods` block's
1222
+ # `Name::ClassMethods` counts only for a concern `Name` ({Scope::ResolutionChain.const_get_candidate?}).
1223
+ def const_get_constant(scope, owner, raw)
1224
+ chain = Scope::ResolutionChain.for(scope, owner, :instance, :methods)
1225
+ found = chain.search(scope) do |entry|
1226
+ names = entry.external? ? entry.candidates : [entry.name]
1227
+ names.map { |name| [name, "#{name}::#{raw}"] }.find do |name, candidate|
1228
+ scope.known_user_class?(candidate) &&
1229
+ Scope::ResolutionChain.const_get_candidate?(scope.discovery, name, candidate)
1230
+ end&.last
1231
+ end
1232
+ found || (raw if scope.known_user_class?(raw))
1233
+ end
1234
+
1235
+ # The `[written, lookup]` pairs of a module's hook-extend entries; `lookup` is `:const_get` for a concern's.
1236
+ # Issue #3005 — a gated concern-block entry counts only for a concern `owner`.
1237
+ def hook_extends(scope, owner)
1238
+ scope.parameter_envelopes_of(owner).each_key.filter_map do |key|
1239
+ next unless key.is_a?(Array) && key.first == Scope::DiscoveryIndex::ENVELOPE_HOOK_EXTEND_KIND
1240
+ next unless Scope::ResolutionChain.effective_hook(scope.discovery, owner, key[2])
1241
+
1242
+ key[1, 2]
1243
+ end
1244
+ end
1245
+
1246
+ # A macro's owner holds the macro's `def`, so it is never an external entry.
1247
+ def macro_entry_defines?(scope, entry, macro)
1248
+ !entry.external? &&
1249
+ scope.parameter_envelopes_of(entry.name).key?([Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND,
1250
+ entry.side, macro])
1251
+ end
1252
+
1253
+ # The classes past a class object's own singleton chain, whose instance methods it answers. A module's chain
1254
+ # does not reach `Class`, but a module body calling a `Class` macro raises as it loads, so it is not told apart.
1255
+ UNIVERSAL_MACRO_OWNERS = %w[Class Module Object Kernel BasicObject].freeze
1256
+ private_constant :UNIVERSAL_MACRO_OWNERS
1257
+
1258
+ def universal_macro_defines?(scope, macro)
1259
+ definer = [Scope::DiscoveryIndex::ENVELOPE_COMPUTED_DEFINER_KIND, :instance, macro]
1260
+ UNIVERSAL_MACRO_OWNERS.any? { |owner| scope.parameter_envelopes_of(owner).key?(definer) }
1261
+ end
1262
+
989
1263
  # Issue #1722 — a `method_missing`, or a project mixin, on one of the class's RBS ancestors
990
1264
  # ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
1265
+ # Issue #2792 — a `method_missing` RBS declares on the class or an ancestor, other than the universal one.
991
1266
  def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
1267
+ side = kind == :singleton ? :singleton : :instance
1268
+ return true if RbsReceiverSurface.rbs_missing_hook?(scope, class_name, side)
1269
+
992
1270
  if kind == :singleton
993
1271
  RbsReceiverSurface.singleton_hook?(scope, class_name) ||
994
1272
  RbsReceiverSurface.singleton_mixin_may_answer?(scope, class_name, method_name)
@@ -1342,14 +1620,14 @@ module Rigor
1342
1620
  # (`METACLASS_ARMS`, "a `plugin_class : Class` really holds a `Plugin` subclass with `.manifest`");
1343
1621
  # the scalar rule enumerated `Module`'s own RBS instead and reported them.
1344
1622
  #
1345
- # Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has eight
1346
- # other callers, and #742 had no evidence that any of them spoke on these receivers.
1623
+ # Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has other
1624
+ # callers, and #742 had no evidence that any of them spoke on these receivers.
1347
1625
  #
1348
1626
  # Issue #879 supplied it for exactly one — `call.wrong-arity`, which decided `v.hexdigest(1, 2, 3)`
1349
1627
  # against `Digest::Instance`'s declaration on the same run where `v.no_such_method_zzz` was declined
1350
1628
  # as unenumerable. One receiver, one run, two answers about whether that surface can be read. The
1351
1629
  # reporter could make no other caller speak: `raise` on a module- or `Class`-typed operand is silent,
1352
- # and `Class`-typed `.new` with a wrong arity is silent too, so the other seven stay on
1630
+ # and `Class`-typed `.new` with a wrong arity is silent too, so the other callers stay on
1353
1631
  # `unbounded_receiver_surface?` until something demonstrates otherwise.
1354
1632
  #
1355
1633
  # Arity takes {#unenumerable_receiver_shape?}, not this whole predicate: ADR-26's open receivers are
@@ -1382,13 +1660,19 @@ module Rigor
1382
1660
  # answers, that never reaches a verdict — depend on `Widget`'s declaring file.
1383
1661
  # `dependency_recorder_spec` caught it here exactly as it caught #723's walk.
1384
1662
  #
1385
- # The receiver's OWN includes only. A project ancestor's unknown include makes the surface just as
1386
- # unknown; walking the chain is a deliberate deferral rather than an oversight.
1387
- def unknown_mixin_includer?(class_name, scope)
1663
+ # A constant the environment does not know is read off the receiver's OWN includes only. Issue #2991 — a
1664
+ # mixin no name spells (`include(*MODS)`, `include helper`) opens the receiver when it sits anywhere on its
1665
+ # instance-side chain, its own level or an ancestor's: the chain marks it (`ResolutionChain#unnamed_mixin?`),
1666
+ # and any name may come from the unlisted module (a hook's `base.include M` is read so too). `self.include M`
1667
+ # and a receiverless `send(:include, M)` are recorded as `include M` is (#2995), so the ordinary lookup reads
1668
+ # `M`. A mixin whose ORDER alone is unknown (`include M; prepend M`) has its module recorded, so it opens
1669
+ # nothing. A class-object receiver reads its singleton chain instead ({#singleton_mixin_may_answer?}).
1670
+ def unknown_mixin_includer?(class_name, scope, kind)
1388
1671
  includes = scope.includes_of(class_name)
1389
- return false if includes.empty?
1672
+ return true if includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
1673
+ return false if kind == :singleton
1390
1674
 
1391
- includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
1675
+ Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).unnamed_mixin?
1392
1676
  end
1393
1677
 
1394
1678
  def known_mixin?(raw_name, class_name, scope)
@@ -1451,14 +1735,16 @@ module Rigor
1451
1735
  end
1452
1736
 
1453
1737
  # ADR-67 WD6b — the argument-position analogue: true when `arg` is rooted at an inferred-parameter
1454
- # local. The argument-type-mismatch rule declines on it for the same lower-bound reason.
1738
+ # local. The argument-type-mismatch rule declines on it for the same lower-bound reason. `scope` is the
1739
+ # call's, and the receiver and any earlier argument run before `arg` and may rebind what a conditional's
1740
+ # predicate in it reads (`m((c = false; 1), c ? param : "x")`), so a predicate there is never folded.
1455
1741
  def inferred_param_argument?(arg, scope)
1456
- inferred_param_rooted?(arg, scope)
1742
+ inferred_param_rooted?(arg, scope, fold: false)
1457
1743
  end
1458
1744
 
1459
1745
  # ADR-67 WD6b — see {CheckRules::InferredParamGuard} for the shared root-walk contract.
1460
- def inferred_param_rooted?(node, scope)
1461
- InferredParamGuard.rooted?(node, scope)
1746
+ def inferred_param_rooted?(node, scope, fold: true)
1747
+ InferredParamGuard.rooted?(node, scope, fold: fold)
1462
1748
  end
1463
1749
 
1464
1750
  def constant_class_name(value)
@@ -1498,8 +1784,8 @@ module Rigor
1498
1784
  # as a per-instance singleton method and answers the rest through a private `method_missing` (ostruct 0.6), so
1499
1785
  # no signature can enumerate its surface. The only other declaration in the environment is json's add-on
1500
1786
  # reopening (`json_create` / `as_json` / `to_json`), which made the class read as fully declared and every
1501
- # field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`, and
1502
- # an RBS-declared `method_missing` does not by itself open a receiver here, so the class is named. No load
1787
+ # field access a `call.undefined-method`. `data/core_overlay/ostruct.rbs` declares that `method_missing`; since
1788
+ # #2792 that alone keeps `call.undefined-method` silent, but the rules below still need the class named. No load
1503
1789
  # gate is needed: the core overlay loads into every environment that has RBS at all, so the list is active
1504
1790
  # whenever the declaration it protects is. Both sides of the class are open, as with
1505
1791
  # {GEM_OVERLAY_OPEN_RECEIVERS}, and an inherited Object / Kernel signature stops deciding `call.wrong-arity`
@@ -1530,6 +1816,97 @@ module Rigor
1530
1816
  ruby_library_reopen?(class_name, scope)
1531
1817
  end
1532
1818
 
1819
+ # Issue #2438 — a class-method call on a class the project's own signatures name but whose source leaves
1820
+ # the singleton surface open. Before `rigor sig-gen --write` such a class was source-only and an
1821
+ # unresolved call on it was silent; the sidecar must not turn the same call into a finding. Two shapes:
1822
+ #
1823
+ # - `C.instance` where `C`, or a project class up its superclass chain, `include`s the stdlib `Singleton`:
1824
+ # the answer is `SingletonMixinDispatch.answers?`, the predicate the typer itself uses, so the rule never
1825
+ # reports a call the engine typed. The class's other members are irrelevant: the mixin's hook defines
1826
+ # `instance`.
1827
+ # - every project declaration of `C` is member-less (`RbsLoader#memberless_project_class?`) AND the source
1828
+ # leaves `C` open ({#open_source_class?}). A member-less declaration does not describe the class's
1829
+ # surface, but a class whose whole definition Rigor can see in `paths:` is still closed, so a typo on it
1830
+ # keeps firing. Instance-side calls keep being checked; declaring any member re-closes the class.
1831
+ def open_project_singleton?(receiver_type, class_name, call_node, scope)
1832
+ return false unless receiver_type.is_a?(Type::Singleton)
1833
+ return true if Inference::MethodDispatcher::SingletonMixinDispatch.answers?(
1834
+ scope, receiver_type, call_node.name, call_node.arguments.nil?
1835
+ )
1836
+
1837
+ loader = scope.environment&.rbs_loader
1838
+ return false if loader.nil? || !loader.respond_to?(:memberless_project_class?)
1839
+
1840
+ loader.memberless_project_class?(class_name) && open_source_class?(class_name, scope, loader)
1841
+ end
1842
+
1843
+ OPENING_HOOKS = %i[inherited included extended prepended method_missing respond_to_missing?].freeze
1844
+ private_constant :OPENING_HOOKS
1845
+
1846
+ CLOSED_ROOTS = Set["Object", "BasicObject"].freeze
1847
+ private_constant :CLOSED_ROOTS
1848
+
1849
+ # Issue #2438 — whether a class's singleton surface may hold methods its source in `paths:` does not show,
1850
+ # read off `Scope::ResolutionChain` (`:constants` flavor, so a project module that holds only constants is
1851
+ # still a project entry). Open when the class, or a project class up its superclass chain:
1852
+ #
1853
+ # 1. `extend`s a module (any singleton-side entry beyond the classes' own singletons), or records an
1854
+ # `inherited` / `included` / `extended` / `prepended` hook or `method_missing` / `respond_to_missing?`.
1855
+ # A plain `include` or `prepend` adds instance methods only, so it leaves the singleton side closed;
1856
+ # 2. has no `class` / `module` body of its own inside `paths:` (`class Ext::Foo` names `Ext` without one);
1857
+ # 3. or the chain reaches a class the project does not define, other than `Object` / `BasicObject`: an
1858
+ # external superclass (`ApplicationRecord < ActiveRecord::Base`, `MyError < StandardError`), a
1859
+ # superclass written as an expression, or — where the source writes no superclass for the last class —
1860
+ # the superclass its member-less declaration writes, unless that is a project class.
1861
+ #
1862
+ # A definition or reopening in a file outside `paths:` cannot be seen, so a class whose body is inside
1863
+ # `paths:` and whose methods live only outside it counts as closed.
1864
+ def open_source_class?(class_name, scope, loader)
1865
+ name = class_name.to_s.delete_prefix("::")
1866
+ instance = Scope::ResolutionChain.for(scope, name, :instance, :constants)
1867
+ singleton = Scope::ResolutionChain.for(scope, name, :singleton, :constants)
1868
+ [instance, singleton].each { |chain| chain.record(scope) }
1869
+ classes = instance.level_classes.compact
1870
+ return true if classes.empty? || instance.truncated?
1871
+
1872
+ sources = scope.discovered_class_sources
1873
+ return true if classes.any? { |klass| (sources[klass] || []).empty? || opening_hook?(klass, scope) }
1874
+ return true if opening_entry?(instance, classes, mixins: false) ||
1875
+ opening_entry?(singleton, classes, mixins: true)
1876
+
1877
+ open_tail?(instance, classes.last, scope, loader)
1878
+ end
1879
+
1880
+ # An external superclass other than `Object` / `BasicObject` opens either side; a module entry opens the
1881
+ # chain only where `mixins` is set (the singleton side, where it is an `extend`).
1882
+ def opening_entry?(chain, classes, mixins:)
1883
+ chain.entries.any? do |entry|
1884
+ if entry.external? && entry.superclass_edge
1885
+ !CLOSED_ROOTS.include?(entry.raw.to_s.delete_prefix("::"))
1886
+ else
1887
+ mixins && !classes.include?(entry.name)
1888
+ end
1889
+ end
1890
+ end
1891
+
1892
+ def opening_hook?(klass, scope)
1893
+ OPENING_HOOKS.any? do |hook|
1894
+ scope.discovered_method?(klass, hook, :singleton) || scope.discovered_method?(klass, hook, :instance)
1895
+ end
1896
+ end
1897
+
1898
+ # The last class's own superclass, which the chain does not name: an expression the tables cannot resolve,
1899
+ # or one only the member-less declaration writes (the chain ends at the implicit `Object` exactly when
1900
+ # the source writes none).
1901
+ def open_tail?(chain, last, scope, loader)
1902
+ return true if chain.opaque_tail?(scope)
1903
+
1904
+ raw = loader.memberless_project_superclass(last)
1905
+ return false if raw.nil? || CLOSED_ROOTS.include?(raw.delete_prefix("::"))
1906
+
1907
+ !scope.discovered_classes.key?(raw.delete_prefix("::"))
1908
+ end
1909
+
1533
1910
  # Issue #2174 — `class Matrix` reopened with one `#:`-annotated method, and no `matrix` signatures
1534
1911
  # loaded, gives Matrix a declaration holding only that method ({RbsLoader#ruby_library_reopen?}).
1535
1912
  # `Matrix.identity(2)` is correct Ruby, so the class's surface is treated as open: `call.undefined-method`
@@ -1549,38 +1926,51 @@ module Rigor
1549
1926
  registry.open_receiver?(class_name)
1550
1927
  end
1551
1928
 
1552
- # Whether THIS run's RBS environment actually includes one of Rigor's OWN partial declarations of the
1553
- # overlaid gem — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see that
1554
- # constant's comment). `RbsLoader#signature_paths` is the same list `Environment.for_project` builds
1555
- # from `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
1556
- # two of its members can carry that declaration:
1929
+ # Whether THIS run's RBS environment actually includes Rigor's OWN partial declaration of
1930
+ # ACTIVESUPPORT — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see
1931
+ # that constant's comment), and both members of that list are `ActiveSupport` classes, so the gate
1932
+ # asks for the activesupport overlay specifically. Issue #2857 added a SECOND bundled overlay
1933
+ # (`addressable`, eligible under `:rbs_collection` coverage), and a collection project can load it
1934
+ # while the activesupport overlay stays unloaded — keying on "any `data/gem_overlay/` directory"
1935
+ # would then silence `ActiveSupport::Duration#bar`, the ADR-26 false negative this gate exists to
1936
+ # prevent.
1557
1937
  #
1558
- # - a `data/gem_overlay/<gem>/` directory, which is a member if and only if `gem_overlay_paths`
1559
- # picked it (the gem is locked, `:missing` coverage, and its plugin twin is NOT reachable —
1560
- # ADR-72), so the `RbsLoader.under_gem_overlay_root?` prefix match is exact: no other signature
1561
- # source lives under that root;
1562
- # - the bundled plugin twin's own `sig/`, when a project wires it through `signature_paths:` instead
1563
- # of `plugins:`. Before #672 that route loaded BOTH halves and `Duration` collapsed outright, so
1564
- # nothing fired and nothing needed protecting; now the overlay stands down and the twin's
1565
- # declaration is the one that loads — with no manifest behind it, so `open_receiver?` above stays
1566
- # false and this is the only thing standing between a real `Duration` receiver and a false
1567
- # `call.undefined-method` on every member the declaration omits.
1938
+ # `RbsLoader#signature_paths` is the same list `Environment.for_project` builds from
1939
+ # `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
1940
+ # two of its members can carry the activesupport declaration:
1568
1941
  #
1569
- # When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the second
1570
- # goes true — its `sig/` rides in as a plugin signature path — but nothing here decides that case:
1571
- # `open_receiver?`'s manifest-driven check short-circuits above it. The two answers agree, which is
1572
- # the point; this predicate asks only whether a Rigor-authored partial declaration is in the
1573
- # environment, never how the project asked for it.
1942
+ # - `data/gem_overlay/activesupport/` itself — a member if and only if `gem_overlay_paths` picked
1943
+ # it (the gem is locked, its coverage source admits the overlay — `:missing` for activesupport —
1944
+ # and its plugin twin is NOT reachable, ADR-72). {RbsLoader.gem_overlay_sig_paths} rebuilds the
1945
+ # exact Pathname `for_project` appended, so a plain `include?` is the test: no other signature
1946
+ # source can occupy that path;
1947
+ # - the bundled activesupport plugin twin's own `sig/`, when a project wires it through
1948
+ # `signature_paths:` instead of `plugins:`. Before #672 that route loaded BOTH halves and
1949
+ # `Duration` collapsed outright, so nothing fired and nothing needed protecting; now the overlay
1950
+ # stands down and the twin's declaration is the one that loads — with no manifest behind it, so
1951
+ # `open_receiver?` above stays false and this is the only thing standing between a real
1952
+ # `Duration` receiver and a false `call.undefined-method` on every member the declaration
1953
+ # omits. {RbsLoader.gem_overlay_twin_signatures_loaded?} enumerates `GEM_OVERLAY_PLUGIN_IDS`'
1954
+ # values, which today is only the activesupport twin; a second overlay twin would need the same
1955
+ # per-gem keying this method now applies to the directory arm.
1574
1956
  #
1575
- # The two arms are asked in that order, over the WHOLE list each time, rather than interleaved per
1576
- # path: the overlay arm is a string prefix and the twin arm canonicalises paths on the filesystem,
1577
- # so the overwhelmingly common overlay-only run never reaches a syscall here.
1957
+ # When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the
1958
+ # second goes true — its `sig/` rides in as a plugin signature path — but nothing here decides
1959
+ # that case: `open_receiver?`'s manifest-driven check short-circuits above it. The two answers
1960
+ # agree, which is the point; this predicate asks only whether the Rigor-authored activesupport
1961
+ # declaration is in the environment, never how the project asked for it.
1962
+ #
1963
+ # The two arms are asked in that order, over the WHOLE list each time, rather than interleaved
1964
+ # per path: the overlay arm is a Pathname comparison (plus the two `File.directory?` probes
1965
+ # {RbsLoader.gem_overlay_sig_paths} needs to rebuild the path) and the twin arm canonicalises
1966
+ # paths on the filesystem, so an overlay-only run pays the cheaper arm first.
1578
1967
  def gem_overlay_loaded?(scope)
1579
1968
  loader = scope.environment&.rbs_loader
1580
1969
  return false if loader.nil? || !loader.respond_to?(:signature_paths)
1581
1970
 
1582
1971
  paths = loader.signature_paths
1583
- paths.any? { |path| Rigor::Environment::RbsLoader.under_gem_overlay_root?(path) } ||
1972
+ activesupport_overlay = Rigor::Environment::RbsLoader.gem_overlay_sig_paths(["activesupport"])
1973
+ paths.any? { |path| activesupport_overlay.include?(path) } ||
1584
1974
  Rigor::Environment::RbsLoader.gem_overlay_twin_signatures_loaded?(paths)
1585
1975
  end
1586
1976
 
@@ -1663,6 +2053,16 @@ module Rigor
1663
2053
  method_def
1664
2054
  end
1665
2055
 
2056
+ # Issue #2786 — {#trustworthy_signature} on the call's receiver as {CallFacts} types it, resolved once per
2057
+ # call node: `call.wrong-arity` and `call.argument-type-mismatch` both ask it with that receiver type and its
2058
+ # concrete class.
2059
+ def receiver_signature(facts)
2060
+ facts.receiver_signature do
2061
+ receiver_type = facts.receiver_type
2062
+ trustworthy_signature(receiver_type, concrete_class_name(receiver_type), facts.call_node, facts.scope)
2063
+ end
2064
+ end
2065
+
1666
2066
  def unauthoritative_inherited_signature?(class_name, method_def, scope)
1667
2067
  return false unless unbounded_receiver_surface?(class_name, scope)
1668
2068
  return true unless method_def.respond_to?(:defined_in)
@@ -1845,29 +2245,28 @@ module Rigor
1845
2245
  # Slice 7 phase 11 — wrong-arity diagnostic. Fires when
1846
2246
  # an explicit-receiver `Prism::CallNode` resolves to a
1847
2247
  # method whose declared overloads do not admit the
1848
- # supplied positional argument count. The rule applies
1849
- # ONLY to the simplest overload shape (single overload,
1850
- # no `rest_positionals`, no keyword parameters, no
1851
- # block-required positionals); calls with `*splat`
1852
- # arguments, keyword arguments, or block-pass arguments
1853
- # are silently skipped to avoid false positives. The
1854
- # check piggybacks on the same scope-index lookup used
1855
- # by `undefined_method_diagnostic`; it returns nil
1856
- # when the call's receiver / RBS coverage / call shape
1857
- # disqualifies the rule.
2248
+ # supplied positional argument count (across all
2249
+ # overloads, see {#compute_arity_envelope}). One overload
2250
+ # with required keyword arguments or trailing positionals
2251
+ # leaves the whole method unchecked; calls with `*splat`, keyword, or
2252
+ # block-pass arguments are silently skipped to avoid
2253
+ # false positives. The check piggybacks on the same
2254
+ # scope-index lookup used by `undefined_method_diagnostic`;
2255
+ # it returns nil when the call's receiver / RBS coverage /
2256
+ # call shape disqualifies the rule.
1858
2257
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1859
- def wrong_arity_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2258
+ def wrong_arity_diagnostic(path, call_node, facts, lexical_sites = nil)
1860
2259
  return nil if call_node.receiver.nil?
1861
2260
  return nil unless plain_positional_call?(call_node)
1862
2261
 
1863
- scope = scope_index[call_node]
2262
+ scope = facts.scope
1864
2263
  return nil if scope.nil?
1865
2264
 
1866
2265
  # ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound; a wrong-arity
1867
2266
  # firing against it is an FP by construction. Decline.
1868
2267
  return nil if inferred_param_receiver?(call_node, scope)
1869
2268
 
1870
- receiver_type = scope.type_of(call_node.receiver)
2269
+ receiver_type = facts.receiver_type
1871
2270
  class_name = concrete_class_name(receiver_type)
1872
2271
  return nil if class_name.nil?
1873
2272
 
@@ -1888,7 +2287,7 @@ module Rigor
1888
2287
  # false positive. Skip arity-checking the chained position.
1889
2288
  return nil if anonymous_struct_new_call?(call_node, class_name, kind)
1890
2289
 
1891
- arity_envelope = arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
2290
+ arity_envelope = arity_envelope_for(facts, class_name, kind)
1892
2291
  return nil if arity_envelope.nil?
1893
2292
 
1894
2293
  actual = (call_node.arguments&.arguments || []).size
@@ -1919,11 +2318,14 @@ module Rigor
1919
2318
  # declaration. `trustworthy_signature` is the same lookup `argument_type_diagnostic` treats as
1920
2319
  # authoritative over a source `def` (see its own comment beside that call), and the two rules must agree
1921
2320
  # on whose contract binds, so a declared method is checked against its declaration.
1922
- def arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
2321
+ def arity_envelope_for(facts, class_name, kind)
2322
+ receiver_type = facts.receiver_type
2323
+ call_node = facts.call_node
2324
+ scope = facts.scope
1923
2325
  if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1924
2326
  return nil unless definition_available?(receiver_type, class_name, scope)
1925
2327
 
1926
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
2328
+ method_def = receiver_signature(facts)
1927
2329
  if method_def && !Rigor::RbsExtended.inferred_signature?(method_def)
1928
2330
  return nil if undeclared_constructor?(class_name, call_node, kind, method_def)
1929
2331
 
@@ -2108,7 +2510,7 @@ module Rigor
2108
2510
  # and union receivers where every member already
2109
2511
  # disqualifies the call (avoid duplicating the
2110
2512
  # undefined-method diagnostic).
2111
- def nil_receiver_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2513
+ def nil_receiver_diagnostic(path, call_node, facts, lexical_sites = nil)
2112
2514
  return nil if call_node.receiver.nil?
2113
2515
  # Safe-navigation calls (`recv&.method`) already
2114
2516
  # short-circuit on nil at runtime, so a nil-bearing
@@ -2122,7 +2524,7 @@ module Rigor
2122
2524
  # produce false positives we cannot suppress.
2123
2525
  return nil unless call_node.receiver.is_a?(Prism::LocalVariableReadNode)
2124
2526
 
2125
- scope = scope_index[call_node]
2527
+ scope = facts.scope
2126
2528
  return nil if scope.nil?
2127
2529
 
2128
2530
  # ADR-58 WD1 — a receiver whose `nil` constituent is purely
@@ -2148,7 +2550,7 @@ module Rigor
2148
2550
  # construction. Decline.
2149
2551
  return nil if scope.inferred_param?(call_node.receiver.name)
2150
2552
 
2151
- receiver_type = scope.type_of(call_node.receiver)
2553
+ receiver_type = facts.receiver_type
2152
2554
  return nil unless receiver_type.is_a?(Type::Union)
2153
2555
 
2154
2556
  # The rule only fires when the analyzer has access to
@@ -2157,7 +2559,7 @@ module Rigor
2157
2559
  return nil unless Rigor::Reflection.rbs_class_known?("NilClass", scope: scope)
2158
2560
 
2159
2561
  return nil unless nil_bearing_union_witnesses?(receiver_type, call_node.name, scope)
2160
- return nil if nil_receiver_withheld?(call_node, receiver_type, scope, scope_index, lexical_sites)
2562
+ return nil if nil_receiver_withheld?(call_node, receiver_type, scope, facts.scope_index, lexical_sites)
2161
2563
 
2162
2564
  build_nil_receiver_diagnostic(path, call_node)
2163
2565
  end
@@ -2196,8 +2598,7 @@ module Rigor
2196
2598
  # nil so the rule bails (silent). Every other receiver (including a
2197
2599
  # `T | nil` union, which already has no single concrete class) flows
2198
2600
  # through unchanged.
2199
- def safe_navigation_receiver(call_node, scope)
2200
- receiver_type = scope.type_of(call_node.receiver)
2601
+ def safe_navigation_receiver(call_node, receiver_type)
2201
2602
  return receiver_type unless call_node.safe_navigation?
2202
2603
  return receiver_type unless nil_member?(receiver_type)
2203
2604
 
@@ -2519,22 +2920,24 @@ module Rigor
2519
2920
  INTEGER_RAISING_OPERATORS = %i[/ % div modulo divmod].freeze
2520
2921
  private_constant :INTEGER_RAISING_OPERATORS
2521
2922
 
2522
- def always_raises_diagnostic(path, call_node, scope_index, lexical_sites = nil)
2523
- return nil unless integer_zero_division?(call_node, scope_index)
2923
+ def always_raises_diagnostic(path, call_node, facts, lexical_sites = nil, raise_shield = nil)
2924
+ return nil unless integer_zero_division?(call_node, facts)
2524
2925
 
2525
2926
  # ADR-121 WD7 — a refinement of `Integer`'s operator (or one Rigor cannot read) in effect may not raise.
2526
- scope = scope_index[call_node]
2527
- return nil if refined_method_in_effect?("Integer", call_node, scope, :instance, lexical_sites)
2927
+ return nil if refined_method_in_effect?("Integer", call_node, facts.scope, :instance, lexical_sites)
2928
+
2929
+ # Issue #3036 — a raise an enclosing `begin` / `def … rescue` catches flows to the handler.
2930
+ return nil if raise_shield&.rescued?(call_node, facts.scope)
2528
2931
 
2529
2932
  build_always_raises_diagnostic(path, call_node)
2530
2933
  end
2531
2934
 
2532
- def integer_zero_division?(call_node, scope_index)
2935
+ def integer_zero_division?(call_node, facts)
2533
2936
  return false unless raising_call_shape?(call_node)
2534
2937
 
2535
- scope = scope_index[call_node]
2938
+ scope = facts.scope
2536
2939
  return false if scope.nil?
2537
- return false unless integer_rooted_for_diagnostic?(scope.type_of(call_node.receiver))
2940
+ return false unless integer_rooted_for_diagnostic?(facts.receiver_type)
2538
2941
 
2539
2942
  arg = single_argument(call_node)
2540
2943
  arg && integer_zero_constant?(scope.type_of(arg))
@@ -2990,8 +3393,7 @@ module Rigor
2990
3393
  # - Elsif chains (`subsequent` is itself an `IfNode`) ARE flagged — the entire downstream chain is
2991
3394
  # unreachable when the outer predicate is a constant literal.
2992
3395
  #
2993
- # Broadening to inferred-constant predicates is queued for a later v0.1.x release once the loop /
2994
- # mutation gaps named above are closed.
3396
+ # Inferred-constant predicates are the `flow.always-truthy-condition` rule's ({AlwaysTruthyConditionCollector}).
2995
3397
  def unreachable_branch_diagnostic(path, node, scope_index)
2996
3398
  scope = scope_index[node]
2997
3399
  return nil if scope.nil?
@@ -3047,17 +3449,17 @@ module Rigor
3047
3449
  # - Issue #1984 — Ruby hands a private call to `method_missing`, so a `method_missing` or
3048
3450
  # `respond_to_missing?` a project entry on the receiver's chain records, on the side the call reaches,
3049
3451
  # declines the rule ({#missing_hook_on_chain?}).
3050
- def visibility_mismatch_diagnostic(path, call_node, scope_index, lexical_sites = nil)
3452
+ def visibility_mismatch_diagnostic(path, call_node, facts, lexical_sites = nil)
3051
3453
  return nil unless explicit_non_self_receiver?(call_node.receiver)
3052
3454
 
3053
- scope = scope_index[call_node]
3455
+ scope = facts.scope
3054
3456
  return nil if scope.nil?
3055
3457
 
3056
3458
  # ADR-67 WD6b — an inferred-parameter receiver's nominal type is an open-call-site lower bound; a
3057
3459
  # private-method visibility firing against it is an FP by construction. Decline.
3058
3460
  return nil if inferred_param_receiver?(call_node, scope)
3059
3461
 
3060
- receiver_type = scope.type_of(call_node.receiver)
3462
+ receiver_type = facts.receiver_type
3061
3463
  case receiver_type
3062
3464
  when Type::Nominal then instance_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
3063
3465
  when Type::Singleton then singleton_visibility_mismatch(path, call_node, scope, receiver_type, lexical_sites)
@@ -3102,7 +3504,14 @@ module Rigor
3102
3504
  build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
3103
3505
  end
3104
3506
 
3105
- PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
3507
+ # ADR-129 WD1 — a run-scoped Memo: one `[discovery, names]` slot keyed on the discovery index's identity, the
3508
+ # only input the name set reads.
3509
+ PRIVATE_NAMES_MEMO = TypingState.memo(
3510
+ :check_rules_private_singleton_names,
3511
+ run_scoped: true, audit: "private singleton names",
3512
+ reason: "one [discovery, names] slot keyed on the discovery index's identity; the name " \
3513
+ "set reads only its method-visibility tables"
3514
+ )
3106
3515
  private_constant :PRIVATE_NAMES_MEMO
3107
3516
 
3108
3517
  # Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
@@ -3113,12 +3522,23 @@ module Rigor
3113
3522
  def private_singleton_candidate?(scope, name)
3114
3523
  DependencyRecorder.read_visibility("*", name) if DependencyRecorder.active?
3115
3524
  discovery = scope.discovery
3116
- slot = Thread.current[PRIVATE_NAMES_MEMO]
3525
+ if Inference::MemoAudit.enabled? && Inference::MemoAudit.suspended?
3526
+ return private_recorded_names(discovery.discovered_method_visibilities).include?(name.to_sym)
3527
+ end
3528
+
3529
+ state = TypingState.current
3530
+ slot = state[PRIVATE_NAMES_MEMO]
3117
3531
  unless slot && slot[0].equal?(discovery)
3118
- slot = [discovery, private_recorded_names(discovery.discovered_method_visibilities)]
3119
- Thread.current[PRIVATE_NAMES_MEMO] = slot
3532
+ names = Inference::MemoAudit.key_audit("private singleton names") do
3533
+ private_recorded_names(discovery.discovered_method_visibilities)
3534
+ end
3535
+ slot = [discovery, names]
3536
+ state[PRIVATE_NAMES_MEMO] = slot
3537
+ return slot[1].include?(name.to_sym)
3120
3538
  end
3121
- slot[1].include?(name.to_sym)
3539
+ Inference::MemoAudit.check("private singleton names", slot[1], name) do
3540
+ private_recorded_names(discovery.discovered_method_visibilities)
3541
+ end.include?(name.to_sym)
3122
3542
  end
3123
3543
 
3124
3544
  def private_recorded_names(tables)
@@ -3381,12 +3801,9 @@ module Rigor
3381
3801
  # - Receiver must be Nominal / Singleton / Constant
3382
3802
  # (the same `concrete_class_name` test).
3383
3803
  # - Method must be in RBS.
3384
- # - Method must have exactly ONE method type
3385
- # (overload). Multi-overload checking is left for
3386
- # a follow-up because picking the "intended"
3387
- # overload requires the dispatcher's full
3388
- # acceptance plumbing.
3389
- # - The selected overload must have NO
3804
+ # - Overloads: a single method type is checked exactly; with several, an argument is a mismatch only
3805
+ # when every overload rejects it ({#multi_overload_argument_mismatch}).
3806
+ # - The overload checked must have NO
3390
3807
  # rest_positionals and NO trailing positionals —
3391
3808
  # the two shapes under which argument N no longer
3392
3809
  # binds to positional parameter N. Keyword
@@ -3430,15 +3847,15 @@ module Rigor
3430
3847
  COERCE_DISPATCH_METHODS = %i[+ - * / % ** & | ^ << >> < > <= >=].to_set.freeze
3431
3848
  private_constant :COERCE_DISPATCH_METHODS
3432
3849
 
3433
- def argument_type_diagnostic(path, call_node, scope_index, lexical_sites = nil)
3850
+ def argument_type_diagnostic(path, call_node, facts, lexical_sites = nil)
3434
3851
  return nil if call_node.receiver.nil?
3435
3852
  return nil if UNIVERSAL_EQUALITY_METHODS.include?(call_node.name)
3436
3853
  return nil unless plain_positional_call?(call_node)
3437
3854
 
3438
- scope = scope_index[call_node]
3855
+ scope = facts.scope
3439
3856
  return nil if scope.nil?
3440
3857
 
3441
- receiver_type = scope.type_of(call_node.receiver)
3858
+ receiver_type = facts.receiver_type
3442
3859
  class_name = concrete_class_name(receiver_type)
3443
3860
  return nil if class_name.nil?
3444
3861
 
@@ -3451,11 +3868,11 @@ module Rigor
3451
3868
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
3452
3869
  return nil unless definition_available?(receiver_type, class_name, scope)
3453
3870
 
3454
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
3871
+ method_def = receiver_signature(facts)
3455
3872
  return nil if method_def.nil?
3456
3873
 
3457
3874
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
3458
- mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, scope_index)
3875
+ mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, facts.scope_index)
3459
3876
  return nil if mismatch.nil?
3460
3877
  return nil if argument_report_withheld?(receiver_type, class_name, call_node, mismatch, scope, lexical_sites)
3461
3878
 
@@ -3603,12 +4020,36 @@ module Rigor
3603
4020
  # The non-nil channel (ADR-64 WD2/WD3): a single-concrete-class
3604
4021
  # argument no overload admits, on a non-coerce method.
3605
4022
  def non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
4023
+ return class_object_overload_mismatch(arg, arg_type, params, param_overrides, scope) if
4024
+ arg_type.is_a?(Type::Singleton)
3606
4025
  return nil unless single_concrete_arg_class?(arg_type, scope)
3607
4026
  return nil if params.any? { |param| param_accepts_arg_class?(param, arg_type, param_overrides, scope) }
3608
4027
 
3609
4028
  { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
3610
4029
  end
3611
4030
 
4031
+ # Issue #2828 — the class-object twin of the non-nil channel. A `singleton(T)` overload the `resolv` overlay
4032
+ # prepends turned `getresources(d, String)`'s one-overload check (`expected singleton(Resolv::DNS::Query)`)
4033
+ # into this multi-overload one, which declined every class object. It fires only when every overload's
4034
+ # parameter at the position is a `singleton(...)` (a `singleton(T)` read through its class bound, see
4035
+ # `SingletonTypeParams.selection_param`) that provably refuses the argument's class; any other parameter
4036
+ # shape admits.
4037
+ def class_object_overload_mismatch(arg, arg_type, params, param_overrides, scope)
4038
+ return nil unless Rigor::Reflection.rbs_class_known?(arg_type.class_name, scope: scope)
4039
+ return nil unless params.all? do |param|
4040
+ !param_overrides[param.name] && param.type.is_a?(RBS::Types::ClassSingleton)
4041
+ end
4042
+ return nil unless params.all? { |param| class_singleton_refuses?(param.type, arg_type, scope) }
4043
+
4044
+ expected = params.map { |param| param.type.to_s.sub("singleton(::", "singleton(") }.uniq.join(" | ")
4045
+ { node: arg, name: nil, expected: expected, actual: arg_type }
4046
+ end
4047
+
4048
+ def class_singleton_refuses?(rbs_type, arg_type, scope)
4049
+ translated = translate_param_type(rbs_type, scope.environment)
4050
+ translated.is_a?(Type::Singleton) && Inference::Acceptance.accepts(translated, arg_type, mode: :gradual).no?
4051
+ end
4052
+
3612
4053
  # The matching positional RBS param across every overload, or nil when
3613
4054
  # any overload has no param at `index` (arity divergence — the
3614
4055
  # wrong-arity rule's concern, not this one's).
@@ -3629,6 +4070,7 @@ module Rigor
3629
4070
  # Load-bearing since rbs 4.1 rewrote core signatures into this form
3630
4071
  # (`Array#fetch`'s block overload is `[I < _ToInt, T] (I index) { … }`).
3631
4072
  def resolve_param_bounds(param, method_type)
4073
+ param = Inference::MethodDispatcher::SingletonTypeParams.selection_param(param, method_type)
3632
4074
  bounded = method_type.type_params.select(&:upper_bound)
3633
4075
  return param if bounded.empty?
3634
4076
 
@@ -3915,6 +4357,7 @@ module Rigor
3915
4357
  param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3916
4358
  return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
3917
4359
  return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
4360
+ return nil if gradual_narrowing_accepted?(arg, arg_type, param_type, scope)
3918
4361
  return nil unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
3919
4362
  # Issue #1362 — a value that mixes such a global with something else (`c ? $stdout : StringIO.new`) is judged
3920
4363
  # without the declared members the file never writes, as it was before the join.
@@ -3952,10 +4395,34 @@ module Rigor
3952
4395
  Inference::Acceptance.accepts(param_type, type, mode: :gradual).no?
3953
4396
  end
3954
4397
 
4398
+ # Issue #2656 — a local a class guard narrowed from a `Dynamic` value
4399
+ # ({DeclarationSourcedGuard.gradual_narrowed?}) is judged by its class, not by the literals the parameter
4400
+ # names: `mode(x) if x.is_a?(String)` with `mode: ("r" | "w")` is accepted, as the gradual value was, while
4401
+ # `num(x) if x.is_a?(String)` with `num: (1 | 2)` still reports, since no String is ever `1` or `2`. The
4402
+ # parameter's value-pinned members are widened to their classes ({Type::Combinator.widen_value_pinned}) and
4403
+ # the narrowed type is judged against that. The overload path ({#non_nil_arg_overload_mismatch}) already
4404
+ # judges by class, so it needs no such reading.
4405
+ #
4406
+ # A literal member of the narrowed type did not come from the gradual value (`x = h[:m] || "r"` narrows to
4407
+ # `"r" | String`), so it is judged against the parameter as written.
4408
+ def gradual_narrowing_accepted?(arg, arg_type, param_type, scope)
4409
+ return false unless DeclarationSourcedGuard.gradual_narrowed?(arg, scope)
4410
+
4411
+ members = arg_type.is_a?(Type::Union) ? arg_type.members : [arg_type]
4412
+ literals, rest = members.partition { |member| member.is_a?(Type::Constant) }
4413
+ if literals.any? { |member| Inference::Acceptance.accepts(param_type, member, mode: :gradual).no? }
4414
+ return false
4415
+ end
4416
+ return true if rest.empty?
4417
+
4418
+ widened = Type::Combinator.widen_value_pinned(param_type)
4419
+ !Inference::Acceptance.accepts(widened, Type::Combinator.union(*rest), mode: :gradual).no?
4420
+ end
4421
+
3955
4422
  # The parameter rejects the argument AND the rejection is not a
3956
4423
  # withheld declaration-sourced-nil case.
3957
4424
  def argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
3958
- return false unless Inference::Acceptance.accepts(param_type, arg_type, mode: :gradual).no?
4425
+ return false unless rejects_beyond_class_objects?(param_type, arg_type)
3959
4426
 
3960
4427
  # ADR-58 (N2 extension) — the same declaration-sourced-nil-is-not-
3961
4428
  # diagnostic-fuel criterion that governs `possible-nil-receiver`
@@ -3971,6 +4438,31 @@ module Rigor
3971
4438
  !declaration_sourced_nil_only_mismatch?(arg, arg_type, param_type, scope)
3972
4439
  end
3973
4440
 
4441
+ # Issue #2855 — the parameter rejects the argument, and not only for its `Class` / `Module` members. An
4442
+ # unparameterised `Class` or `Module` nominal ({Acceptance.class_or_module_nominal?}) against a parameter
4443
+ # that admits class objects (`singleton(X)`, alone or in a union) is a class object whose identity inference
4444
+ # lost — a `const_get` result kept by `is_a?(Class)`, an `is_a?(Module)` guard — so it is withheld. The rest
4445
+ # of the argument is still judged: `Class | Integer` against `singleton(X)` reports for its `Integer`.
4446
+ # Acceptance itself keeps answering `no`, so overload selection is unchanged.
4447
+ def rejects_beyond_class_objects?(param_type, arg_type)
4448
+ return false unless Inference::Acceptance.accepts(param_type, arg_type, mode: :gradual).no?
4449
+ return true unless admits_class_objects?(param_type)
4450
+
4451
+ members = arg_type.is_a?(Type::Union) ? arg_type.members : [arg_type]
4452
+ rest = members.reject { |member| Inference::Acceptance.class_or_module_nominal?(member) }
4453
+ return true if rest.size == members.size
4454
+
4455
+ Inference::Acceptance.accepts(param_type, Type::Combinator.union(*rest), mode: :gradual).no?
4456
+ end
4457
+
4458
+ def admits_class_objects?(param_type)
4459
+ case param_type
4460
+ when Type::Singleton then true
4461
+ when Type::Union then param_type.members.any?(Type::Singleton)
4462
+ else false
4463
+ end
4464
+ end
4465
+
3974
4466
  # True when `arg` is a declaration-sourced read whose rejection is
3975
4467
  # caused solely by its nil constituent: stripping nil from the argument
3976
4468
  # type yields a type the parameter accepts (gradual mode). Mirrors the
@@ -3986,7 +4478,12 @@ module Rigor
3986
4478
  non_nil = Type::Combinator.union(*arg_type.members.reject { |m| nil_member?(m) })
3987
4479
  return false if non_nil.is_a?(Type::Bot)
3988
4480
 
3989
- Inference::Acceptance.accepts(param_type, non_nil, mode: :gradual).yes?
4481
+ result = Inference::Acceptance.accepts(param_type, non_nil, mode: :gradual)
4482
+ # Issue #2855 — a `Class?` ivar against `singleton(X)` is withheld as its `Class` alone would be.
4483
+ # Issue #2863 — a `maybe` (a project class the host process has not loaded, so its ancestry is unresolved:
4484
+ # `singleton(Circle)` against `singleton(Shape)`) is not a rejection either: the non-nil part alone would not
4485
+ # fire, so the nil is the only reason and it is withheld.
4486
+ !result.no? || !rejects_beyond_class_objects?(param_type, non_nil)
3990
4487
  end
3991
4488
 
3992
4489
  # What this rule needs is that argument N binds to positional
@@ -4096,17 +4593,15 @@ module Rigor
4096
4593
  # OWN declared return, and the sibling override rules gate on `defined_on?` — this one did not.
4097
4594
  # The inherited-declaration signal belongs to the ADR-35 family, which requires both sides
4098
4595
  # authored. Decided in ADR-110 WD3.
4099
- # - Skips methods whose enclosing class isn't a `Type::Singleton` self_type that we can name
4100
- # (top-level / module-level methods land outside the rule).
4596
+ # - Skips methods whose enclosing `self` names no class (top-level methods land outside the rule).
4101
4597
  # - Skips methods whose body's last expression is absent or types as `Dynamic[top]` (the
4102
4598
  # analyzer's fail-soft fallback) — emitting on `Dynamic[top]` would be noise.
4103
4599
  # - Compares the inferred body type against the declared return via `accepts?`:
4104
4600
  # :yes → silent
4105
4601
  # :no → emit at :error (severity_profile may re-stamp; default `balanced` keeps the
4106
4602
  # authored severity).
4107
- # :maybe → emit at :warning. Promoted to :error under `severity_profile: strict` per ADR-8 §
4108
- # "Severity profile".
4109
- def return_type_mismatch_diagnostic(path, def_node, scope_index)
4603
+ # :maybe → silent (see {#compare_return}; ADR-8's `strict` promotion is deferred).
4604
+ def return_type_mismatch_diagnostic(path, def_node, scope_index, replaced_defs = nil)
4110
4605
  return nil if def_node.body.nil?
4111
4606
 
4112
4607
  last_expr = body_last_expression(def_node.body)
@@ -4115,7 +4610,7 @@ module Rigor
4115
4610
  inner_scope = scope_index[last_expr] || scope_index[def_node.body] || scope_index[def_node]
4116
4611
  return nil if inner_scope.nil?
4117
4612
 
4118
- declared = declared_return_type(def_node, scope_index)
4613
+ declared = declared_return_type(def_node, scope_index, replaced_defs)
4119
4614
  return nil if declared.nil?
4120
4615
 
4121
4616
  # Issue #1362 — a body that ends on a builtin global still on its declared seed, or on a local copied from
@@ -4162,14 +4657,18 @@ module Rigor
4162
4657
  # `non-empty-array[Integer]`, etc. — are stricter than the underlying RBS class, so a body whose
4163
4658
  # inferred type the bare RBS sig would accept may still fail the refinement (e.g. `def name; "";
4164
4659
  # end` returns `Constant[""]`, accepted by `String` but rejected by `non-empty-string`).
4165
- def declared_return_type(def_node, scope_index)
4660
+ def declared_return_type(def_node, scope_index, replaced_defs = nil)
4166
4661
  scope = scope_index[def_node]
4167
4662
  return nil if scope.nil?
4168
4663
 
4169
4664
  self_type = scope.self_type
4170
4665
  return nil unless self_type.respond_to?(:class_name)
4171
4666
 
4172
- side = def_node.receiver.nil? ? receiverless_def_side(scope, self_type.class_name, def_node) : :singleton
4667
+ side = if def_node.receiver.nil?
4668
+ receiverless_def_side(scope, self_type.class_name, def_node)
4669
+ else
4670
+ receiver_def_side(scope, self_type.class_name, def_node)
4671
+ end
4173
4672
  return nil if side.nil?
4174
4673
 
4175
4674
  method_def =
@@ -4180,6 +4679,7 @@ module Rigor
4180
4679
  end
4181
4680
  return nil if method_def.nil?
4182
4681
  return nil unless defined_on?(method_def, self_type.class_name)
4682
+ return nil if replaced_receiver_def?(def_node, replaced_defs)
4183
4683
 
4184
4684
  override = Rigor::RbsExtended.read_return_type_override(method_def, environment: scope.environment)
4185
4685
  return override if override
@@ -4202,6 +4702,30 @@ module Rigor
4202
4702
  nil
4203
4703
  end
4204
4704
 
4705
+ # Issue #3155 — a singleton def a later certain def replaced ({ReplacedSingletonDefs}). Asked only once the def
4706
+ # has a signature to answer to, so the file walk runs only then.
4707
+ def replaced_receiver_def?(def_node, replaced_defs)
4708
+ !def_node.receiver.nil? && replaced_defs&.replaced?(def_node)
4709
+ end
4710
+
4711
+ # Issue #3155 — `def self.x`, or `def K.x` in `K`'s body, answers to K's singleton signature. A def on another
4712
+ # object (`def String.q`, `def obj.q` in a class body) or `def self.x` in a `class << self` body (a method of
4713
+ # the singleton class's own singleton) answers to none of K's: `nil`, and the caller declines.
4714
+ # {#declared_return_type} also declines, once a signature is found, a singleton def a later certain def of the
4715
+ # name in the same body replaced (`def K.w`; `def self.w`; {ReplacedSingletonDefs}). `def (self).x` reads as
4716
+ # `def self.x`; `K = self; def K.x` is not read.
4717
+ def receiver_def_side(scope, class_name, def_node)
4718
+ receiver = ReplacedSingletonDefs.unparenthesized(def_node.receiver)
4719
+ lexical_self = if receiver.is_a?(Prism::SelfNode)
4720
+ !scope.singleton_class_body?
4721
+ else
4722
+ Inference::ScopeIndexer.def_receiver_targets_lexical_self?(receiver, class_name.split("::"))
4723
+ end
4724
+ return nil unless lexical_self
4725
+
4726
+ :singleton
4727
+ end
4728
+
4205
4729
  # The tables hold the live node or one re-resolved from a `DefHandle`, so identity is the name and span.
4206
4730
  def same_def_node?(recorded, def_node)
4207
4731
  recorded.is_a?(Prism::DefNode) && recorded.name == def_node.name &&
@@ -4241,9 +4765,9 @@ module Rigor
4241
4765
  # precision improvements.
4242
4766
  def compare_return(declared, inferred)
4243
4767
  result = declared.accepts(inferred)
4244
- return :error if result.no?
4768
+ return nil unless result.no?
4245
4769
 
4246
- nil
4770
+ :error
4247
4771
  end
4248
4772
 
4249
4773
  def build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
@@ -4345,8 +4869,10 @@ module Rigor
4345
4869
  # defined LATER under that name would change the answer; `DefinerResolution` files the negative class
4346
4870
  # edge of each external entry it tests.
4347
4871
  def overridden_ancestors_start(scope, class_name)
4348
- own = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).index_of(class_name.to_s)
4349
- own && (own + 1)
4872
+ # Issue #2979 — past the entries of the names bound to the same class (`K = Struct.new("Named", …)`):
4873
+ # a `def` there redefines the class's method rather than overriding an ancestor's.
4874
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods)
4875
+ chain.ancestors_start(scope, class_name.to_s)
4350
4876
  end
4351
4877
 
4352
4878
  # `[defining_class, visibility]` for the nearest user-source ancestor that defines an instance
@@ -4441,6 +4967,7 @@ module Rigor
4441
4967
  parent_return = declared_return_union(parent_method, scope.environment, type_vars: parent_type_vars)
4442
4968
  return nil if override_return.nil? || parent_return.nil?
4443
4969
  return nil if dynamic_top?(parent_return) # untyped / unbound-generic parent contract
4970
+ return nil if void_over_nil?(override_method, parent_return)
4444
4971
 
4445
4972
  return nil unless parent_return.accepts(override_return).no?
4446
4973
 
@@ -4449,22 +4976,42 @@ module Rigor
4449
4976
  )
4450
4977
  end
4451
4978
 
4979
+ # A `-> void` override of a parent return of exactly `nil` (or `NilClass`) is exempt (#3046): both
4980
+ # say "no useful value", and callers cannot use a void return. Keyed on the declared `void` of every
4981
+ # arm (the translator reads `void` as top, so a `-> top` override still fires); any non-nil parent
4982
+ # return still fires.
4983
+ def void_over_nil?(override_method, parent_return)
4984
+ return false unless nil_type?(parent_return)
4985
+
4986
+ override_method.method_types.all? { |mt| mt.type.return_type.is_a?(::RBS::Types::Bases::Void) }
4987
+ end
4988
+
4989
+ # `nil`, or the nominal `NilClass`, whose only value it is.
4990
+ def nil_type?(type)
4991
+ return type.value.nil? if type.is_a?(Type::Constant)
4992
+
4993
+ type.is_a?(Type::Nominal) && type.class_name == "NilClass" && type.type_args.empty?
4994
+ end
4995
+
4452
4996
  # `[defining_class, RBS::Definition::Method]` for the nearest project-discovered ancestor whose
4453
4997
  # RBS declares `method_name` (not the starting class's own declaration), or nil.
4454
4998
  def nearest_ancestor_method_def(scope, class_name, method_name)
4455
4999
  from = overridden_ancestors_start(scope, class_name)
4456
5000
  return nil if from.nil?
4457
5001
 
5002
+ passed_owners = []
4458
5003
  case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
4459
5004
  question: :override, from: from) do |ancestor|
4460
- method_def = safe_instance_method_definition(ancestor, method_name, scope)
4461
- # The signature text is the answer two worlds compare: an `RBS::Definition::Method` has no
4462
- # structural equality of its own.
4463
- method_def.method_types.map(&:to_s) if method_def && !defined_on?(method_def, class_name)
5005
+ override_parent_answer(scope, ancestor, method_name, passed_owners)
4464
5006
  end
4465
- in Inference::DefinerResolution::Known(owner:)
4466
- method_def = safe_instance_method_definition(owner, method_name, scope)
4467
- method_def && [owner, method_def]
5007
+ in Inference::DefinerResolution::Known(owner:, answer:)
5008
+ # The nearest declaration must be the one that answers: an ancestor asked past for a project
5009
+ # owner the walk never reached (one the Ruby chain does not hold) leaves the rule silent.
5010
+ unless answer.last == RBS_ONLY_OWNER ||
5011
+ passed_owners.any? { |passed| passed != normalize_class_name(owner) }
5012
+ method_def = safe_instance_method_definition(owner, method_name, scope)
5013
+ method_def && [owner, method_def]
5014
+ end
4468
5015
  in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
4469
5016
  nil
4470
5017
  in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
@@ -4472,6 +5019,38 @@ module Rigor
4472
5019
  end
4473
5020
  end
4474
5021
 
5022
+ # The answer an RBS-only owner gives the override walk: it ends the walk, and the rule stays silent.
5023
+ RBS_ONLY_OWNER = :rbs_only_owner
5024
+ private_constant :RBS_ONLY_OWNER
5025
+
5026
+ # One project ancestor's answer to the override walk. The signature text is the answer two worlds
5027
+ # compare: an `RBS::Definition::Method` has no structural equality of its own. Only an ancestor whose
5028
+ # own RBS declares the method answers with it (#3046). One that inherits the declaration from another
5029
+ # project class or module is asked past, so that owner answers at its own entry; the owner is recorded
5030
+ # in `passed_owners`, which the answering owner must match. One that inherits it from an RBS-only owner
5031
+ # the project chain cannot see ends the walk silent ({RBS_ONLY_OWNER}): that owner is the nearest
5032
+ # declaration, and RBS-only ancestors are #130's.
5033
+ def override_parent_answer(scope, ancestor, method_name, passed_owners)
5034
+ method_def = safe_instance_method_definition(ancestor, method_name, scope)
5035
+ return nil if method_def.nil?
5036
+ return method_def.method_types.map(&:to_s) if declared_by?(method_def, ancestor)
5037
+
5038
+ owner = method_def.implemented_in || method_def.defined_in
5039
+ return RBS_ONLY_OWNER unless owner && scope.discovered_classes.key?(normalize_class_name(owner))
5040
+
5041
+ passed_owners << normalize_class_name(owner)
5042
+ nil
5043
+ end
5044
+
5045
+ # True when `ancestor`'s own RBS declares `method_def`: on its body, or through an interface it
5046
+ # includes itself (RBS records the including class as `implemented_in`).
5047
+ def declared_by?(method_def, ancestor)
5048
+ return true if defined_on?(method_def, ancestor)
5049
+ return false unless method_def.defined_in&.interface?
5050
+
5051
+ normalize_class_name(method_def.implemented_in) == normalize_class_name(ancestor)
5052
+ end
5053
+
4475
5054
  def safe_instance_method_definition(class_name, method_name, scope)
4476
5055
  Reflection.instance_method_definition(class_name, method_name, scope: scope)
4477
5056
  rescue StandardError