rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -53,6 +53,24 @@ module Rigor
53
53
  # `else`-only.
54
54
  DEFENSIVE_EXIT_CALLS = %i[raise fail throw abort exit].freeze
55
55
 
56
+ # Issue #2071 — core classes whose idiomatic stand-ins are not their subclasses: a `StringIO` answers the
57
+ # IO protocol without being an `IO`, and a `Tempfile` delegates to a `File` without being one (or an `IO`).
58
+ # A subject Rigor types as one of these classes (`$stdout` by idiom, ADR-117) may hold such a stand-in,
59
+ # which no `when IO` / `in File` matches, so the `else` still runs for it.
60
+ DUCK_STAND_INS = {
61
+ "IO" => %w[StringIO Tempfile].freeze,
62
+ "File" => %w[Tempfile].freeze
63
+ }.freeze
64
+
65
+ # Each stand-in's ancestors as Ruby 4.0.5 has them (`StringIO.ancestors`, `Tempfile.ancestors` without the
66
+ # anonymous `DelegateClass` entries): a clause naming one matches the stand-in. Written out rather than read
67
+ # from RBS, which declares `class Tempfile < File`; a `Tempfile` is a `Delegator`, so neither `Object` nor
68
+ # `Kernel` matches it.
69
+ STAND_IN_ANCESTORS = {
70
+ "StringIO" => %w[StringIO Enumerable Object Kernel BasicObject].freeze,
71
+ "Tempfile" => %w[Tempfile Delegator BasicObject].freeze
72
+ }.freeze
73
+
56
74
  # @!attribute kind
57
75
  # @return `:disjoint` (this clause's type is disjoint
58
76
  # from the still-possible subject), `:prior_exhaustion` (an
@@ -137,9 +155,42 @@ module Rigor
137
155
  end
138
156
  coverage.record(clause)
139
157
  end
158
+ return if stand_in_reaches_else?(node, entry_type)
159
+
140
160
  collect_else(node.else_clause, subject.name, keyword)
141
161
  end
142
162
 
163
+ # Issue #2071 — whether the subject, as the `case` binds it, has a `Nominal` member of a {DUCK_STAND_INS}
164
+ # class with a stand-in no clause names an ancestor of ({STAND_IN_ANCESTORS}). `io = STDOUT; case io when IO
165
+ # then … else … end` runs the `else` for a `StringIO`, which the type `IO` stands for in idiomatic Ruby; a
166
+ # literal subject is a `Constant`, not a `Nominal`, and keeps its verdict. Asked of the `else` only: a dead
167
+ # `when` has its own evidence rules (#1429, #1485).
168
+ def stand_in_reaches_else?(node, entry_type)
169
+ members = entry_type.is_a?(Type::Union) ? entry_type.members : [entry_type]
170
+ stand_ins = members.flat_map do |member|
171
+ member.is_a?(Type::Nominal) ? DUCK_STAND_INS.fetch(member.class_name.delete_prefix("::"), []) : []
172
+ end
173
+ return false if stand_ins.empty?
174
+
175
+ named = clause_class_names(node)
176
+ stand_ins.any? { |stand_in| named.none? { |name| STAND_IN_ANCESTORS.fetch(stand_in).include?(name) } }
177
+ end
178
+
179
+ # The classes the clauses name as bare constants (`when A, B`, `in A`, `in A => x`).
180
+ def clause_class_names(node)
181
+ node.conditions.flat_map do |clause|
182
+ patterns = clause.is_a?(Prism::WhenNode) ? clause.conditions : [bare_pattern(clause.pattern)]
183
+ patterns.filter_map do |pattern|
184
+ entry = @scope_index[pattern]
185
+ entry && Inference::Narrowing.lexical_class_name(pattern, entry)
186
+ end
187
+ end
188
+ end
189
+
190
+ def bare_pattern(pattern)
191
+ pattern.is_a?(Prism::CapturePatternNode) ? pattern.value : pattern
192
+ end
193
+
143
194
  def entry_subject_type(case_node, subject_name)
144
195
  scope = @scope_index[case_node]
145
196
  scope&.local(subject_name)
@@ -229,8 +280,8 @@ module Rigor
229
280
 
230
281
  # The trailing `else` is dead when every subject value is covered by the `when` clauses — the final
231
282
  # falsey scope (recorded on the `else` node) narrows the subject to `bot`. Defensive `else` bodies (a
232
- # bare `raise` / `fail` / `throw`) are deliberate guards, not removable dead code, so they are
233
- # skipped.
283
+ # bare `raise` / `fail` / `throw` / `abort` / `exit`) are deliberate guards, not removable dead code, so
284
+ # they are skipped.
234
285
  def collect_else(else_clause, subject_name, keyword)
235
286
  return if else_clause.nil? || else_clause.statements.nil?
236
287
  return if defensive_else?(else_clause)
@@ -29,7 +29,7 @@ module Rigor
29
29
  class VoidValueUseCollector
30
30
  # The assignment forms whose `value` slot is a value position. `MultiWriteNode` (destructuring) is
31
31
  # intentionally excluded — its `value` is a container the void call would have to be spread through,
32
- # not a direct use, so it is out of this slice's FP-narrow scope.
32
+ # not a direct use, so it is outside the FP-narrow scope.
33
33
  WRITE_NODE_CLASSES = [
34
34
  Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
35
35
  Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
@@ -29,9 +29,11 @@ require_relative "check_rules/void_value_use_collector"
29
29
  require_relative "check_rules/self_closedness_scanner"
30
30
  require_relative "check_rules/source_arity"
31
31
  require_relative "check_rules/lexical_method_sites"
32
+ require_relative "check_rules/rebinding_block_callees"
32
33
  require_relative "check_rules/rbs_receiver_surface"
33
34
  require_relative "check_rules/special_global_setters"
34
35
  require_relative "check_rules/ruby_deprecations"
36
+ require_relative "../run_scoped_slots"
35
37
 
36
38
  module Rigor
37
39
  module Analysis
@@ -52,7 +54,7 @@ module Rigor
52
54
  #
53
55
  # - implicit-self calls (no `node.receiver`) — too noisy
54
56
  # without per-method RBS for every helper in the class.
55
- # - dynamic / unknown receivers (`Dynamic[T]`, `Top`, `Union`)
57
+ # - dynamic / unknown receivers (`Dynamic[T]`, `Top`)
56
58
  # — by definition we cannot enumerate the method set.
57
59
  # - shape carriers: `Tuple` → "Array", `HashShape` → "Hash",
58
60
  # `Constant` → the constant's class — `concrete_class_name`
@@ -85,9 +87,8 @@ module Rigor
85
87
 
86
88
  module_function
87
89
 
88
- # Yields diagnostics for every unrecognised method call on
89
- # a typed receiver in `root`'s subtree. The caller MUST
90
- # have already produced `scope_index` through
90
+ # Returns the diagnostics for `root`'s subtree across the rule
91
+ # catalogue. The caller MUST have already produced `scope_index` through
91
92
  # `Rigor::Inference::ScopeIndexer.index(root, default_scope:)`.
92
93
  #
93
94
  # @param path — used to populate
@@ -450,11 +451,31 @@ module Rigor
450
451
  other_class = ivar_class_for(write[:type])
451
452
  next nil if other_class.nil? || other_class == "NilClass" || other_class == first_class
452
453
  next nil if declaration_admits?(write[:declared], canonical[:type], write[:type])
454
+ next nil if separate_invocations?(write, other_class, writes)
453
455
 
454
456
  build_ivar_write_mismatch_diagnostic(path, write[:node], class_name, ivar_name, first_class, other_class)
455
457
  end
456
458
  end
457
459
 
460
+ # ADR-125 — a divergent write is never live together with the writes it conflicts with when the class
461
+ # serves one entry point per instance (a plugin-declared per-invocation class, e.g. a Rails controller:
462
+ # one action per request) and every conflicting write — non-nil, of a concrete class other than this
463
+ # write's — sits in a DIFFERENT action of the same class. An action is a public method no declared
464
+ # non-action macro names (`before_action :x`, `helper_method :y`); {IvarWriteCollector} decides it. A
465
+ # conflicting write in a private or protected helper, a callback target, a `helper_method`,
466
+ # `initialize`, or the same method keeps the finding. Which helper each action reaches is not proved
467
+ # here.
468
+ def separate_invocations?(write, write_class, writes)
469
+ return false unless write[:action]
470
+
471
+ writes.all? do |other|
472
+ other_class = ivar_class_for(other[:type])
473
+ next true if other_class.nil? || other_class == "NilClass" || other_class == write_class
474
+
475
+ other[:action] && other[:method_name] != write[:method_name]
476
+ end
477
+ end
478
+
458
479
  def declaration_admits?(declared, *write_types)
459
480
  return false if declared.nil?
460
481
 
@@ -729,7 +750,7 @@ module Rigor
729
750
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
730
751
  # gate belongs where that one consult is rather than duplicated into each shape's path.
731
752
  #
732
- # The union side of that placement IS reachable, contrary to what this comment used to say. The
753
+ # The union side of that placement is reachable. The
733
754
  # shape needs a non-nil union of at least two RBS-DECLARED classes — an undeclared arm is an
734
755
  # ADR-26 open receiver and `union_arm_blocks_undefined_fire?` bails before the method lookup,
735
756
  # which is why the earlier probes read silent and were misread as the gate being unreachable.
@@ -776,13 +797,6 @@ module Rigor
776
797
  return union_undefined_method_diagnostic(path, call_node, receiver_type, scope, lexical_sites)
777
798
  end
778
799
 
779
- # ADR-26 — a plugin may declare a class "open": one
780
- # known to respond beyond its RBS-declared method
781
- # surface (e.g. `ActiveRecord::Relation`, which
782
- # delegates an unbounded set of user-defined scopes to
783
- # its model). Flagging an undefined method on a class
784
- # with an open dynamic surface is unsound, so the rule
785
- # skips it.
786
800
  # An unbounded receiver surface: either a plugin-declared
787
801
  # open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
788
802
  # a type Rigor synthesized (a missing-namespace module / a
@@ -818,6 +832,7 @@ module Rigor
818
832
  return nil if method_def
819
833
 
820
834
  return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
835
+ return nil if open_project_singleton?(receiver_type, class_name, call_node, scope)
821
836
  return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
822
837
  call_node.receiver)
823
838
 
@@ -969,7 +984,8 @@ module Rigor
969
984
  # ({RbsReceiverSurface}). Last for {#ancestry_declares_method?}'s reason: it walks the class graph.
970
985
  def rbs_ancestor_may_answer?(class_name, method_name, scope, kind)
971
986
  if kind == :singleton
972
- RbsReceiverSurface.singleton_hook?(scope, class_name)
987
+ RbsReceiverSurface.singleton_hook?(scope, class_name) ||
988
+ RbsReceiverSurface.singleton_mixin_may_answer?(scope, class_name, method_name)
973
989
  else
974
990
  RbsReceiverSurface.ancestor_may_answer?(scope, class_name, method_name)
975
991
  end
@@ -1225,12 +1241,22 @@ module Rigor
1225
1241
  # body is morally a class body, so ADR-34 stays silent there — including on a genuinely
1226
1242
  # undefined name inside the block. Ranges are computed once per file.
1227
1243
  return nil if call_inside_receiver_eval_ranges?(eval_ranges, call_node)
1228
- # ADR-121 WD7 — a refinement of `Object` (or one Rigor cannot read) in effect here may define the name.
1229
- return nil if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
1244
+ return nil if lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index)
1230
1245
 
1231
1246
  build_unresolved_toplevel_diagnostic(path, call_node)
1232
1247
  end
1233
1248
 
1249
+ # The `call.unresolved-toplevel` declines that read the file's syntax, asked last because they walk it.
1250
+ def lexically_declined_toplevel_call?(call_node, scope, lexical_sites, scope_index = nil)
1251
+ # ADR-121 WD7 — a refinement of `Object` (or one Rigor cannot read) in effect here may define the name.
1252
+ return true if refined_method_in_effect?("Object", call_node, scope, :instance, lexical_sites)
1253
+ return false if lexical_sites.nil?
1254
+
1255
+ # Issue #1817 — a block handed to a project method that may `module_eval` / `instance_exec` it runs under
1256
+ # another `self`, so a call in it is not a top-level one ({RebindingBlockCallees}).
1257
+ RebindingBlockCallees.inside_rebound_block?(call_node, scope, lexical_sites.block_calls, scope_index)
1258
+ end
1259
+
1234
1260
  # Issue #1383 — RBS core declares `main`'s private singleton methods on
1235
1261
  # `RBS::Unnamed::TopLevelSelfClass`, so the set follows RBS rather than a hand-kept
1236
1262
  # list. CRuby 4.0's `main` also has `ruby2_keywords`, which RBS does not declare.
@@ -1310,14 +1336,14 @@ module Rigor
1310
1336
  # (`METACLASS_ARMS`, "a `plugin_class : Class` really holds a `Plugin` subclass with `.manifest`");
1311
1337
  # the scalar rule enumerated `Module`'s own RBS instead and reported them.
1312
1338
  #
1313
- # Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has eight
1314
- # other callers, and #742 had no evidence that any of them spoke on these receivers.
1339
+ # Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has other
1340
+ # callers, and #742 had no evidence that any of them spoke on these receivers.
1315
1341
  #
1316
1342
  # Issue #879 supplied it for exactly one — `call.wrong-arity`, which decided `v.hexdigest(1, 2, 3)`
1317
1343
  # against `Digest::Instance`'s declaration on the same run where `v.no_such_method_zzz` was declined
1318
1344
  # as unenumerable. One receiver, one run, two answers about whether that surface can be read. The
1319
1345
  # reporter could make no other caller speak: `raise` on a module- or `Class`-typed operand is silent,
1320
- # and `Class`-typed `.new` with a wrong arity is silent too, so the other seven stay on
1346
+ # and `Class`-typed `.new` with a wrong arity is silent too, so the other callers stay on
1321
1347
  # `unbounded_receiver_surface?` until something demonstrates otherwise.
1322
1348
  #
1323
1349
  # Arity takes {#unenumerable_receiver_shape?}, not this whole predicate: ADR-26's open receivers are
@@ -1419,14 +1445,16 @@ module Rigor
1419
1445
  end
1420
1446
 
1421
1447
  # ADR-67 WD6b — the argument-position analogue: true when `arg` is rooted at an inferred-parameter
1422
- # local. The argument-type-mismatch rule declines on it for the same lower-bound reason.
1448
+ # local. The argument-type-mismatch rule declines on it for the same lower-bound reason. `scope` is the
1449
+ # call's, and the receiver and any earlier argument run before `arg` and may rebind what a conditional's
1450
+ # predicate in it reads (`m((c = false; 1), c ? param : "x")`), so a predicate there is never folded.
1423
1451
  def inferred_param_argument?(arg, scope)
1424
- inferred_param_rooted?(arg, scope)
1452
+ inferred_param_rooted?(arg, scope, fold: false)
1425
1453
  end
1426
1454
 
1427
1455
  # ADR-67 WD6b — see {CheckRules::InferredParamGuard} for the shared root-walk contract.
1428
- def inferred_param_rooted?(node, scope)
1429
- InferredParamGuard.rooted?(node, scope)
1456
+ def inferred_param_rooted?(node, scope, fold: true)
1457
+ InferredParamGuard.rooted?(node, scope, fold: fold)
1430
1458
  end
1431
1459
 
1432
1460
  def constant_class_name(value)
@@ -1487,12 +1515,118 @@ module Rigor
1487
1515
  # overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
1488
1516
  # bundled directory this run actually loaded, a core-overlay
1489
1517
  # open receiver ({CORE_OVERLAY_OPEN_RECEIVERS}), or a
1490
- # Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`).
1518
+ # Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`),
1519
+ # or a project-only reopen of a Ruby library class whose own RBS
1520
+ # did not load ({#ruby_library_reopen?}).
1491
1521
  def unbounded_receiver_surface?(class_name, scope)
1492
1522
  bare_name = class_name.to_s.sub(/\A::/, "")
1493
1523
  open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
1494
1524
  CORE_OVERLAY_OPEN_RECEIVERS.include?(bare_name) ||
1495
- (GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope))
1525
+ (GEM_OVERLAY_OPEN_RECEIVERS.include?(bare_name) && gem_overlay_loaded?(scope)) ||
1526
+ ruby_library_reopen?(class_name, scope)
1527
+ end
1528
+
1529
+ # Issue #2438 — a class-method call on a class the project's own signatures name but whose source leaves
1530
+ # the singleton surface open. Before `rigor sig-gen --write` such a class was source-only and an
1531
+ # unresolved call on it was silent; the sidecar must not turn the same call into a finding. Two shapes:
1532
+ #
1533
+ # - `C.instance` where `C`, or a project class up its superclass chain, `include`s the stdlib `Singleton`:
1534
+ # the answer is `SingletonMixinDispatch.answers?`, the predicate the typer itself uses, so the rule never
1535
+ # reports a call the engine typed. The class's other members are irrelevant: the mixin's hook defines
1536
+ # `instance`.
1537
+ # - every project declaration of `C` is member-less (`RbsLoader#memberless_project_class?`) AND the source
1538
+ # leaves `C` open ({#open_source_class?}). A member-less declaration does not describe the class's
1539
+ # surface, but a class whose whole definition Rigor can see in `paths:` is still closed, so a typo on it
1540
+ # keeps firing. Instance-side calls keep being checked; declaring any member re-closes the class.
1541
+ def open_project_singleton?(receiver_type, class_name, call_node, scope)
1542
+ return false unless receiver_type.is_a?(Type::Singleton)
1543
+ return true if Inference::MethodDispatcher::SingletonMixinDispatch.answers?(
1544
+ scope, receiver_type, call_node.name, call_node.arguments.nil?
1545
+ )
1546
+
1547
+ loader = scope.environment&.rbs_loader
1548
+ return false if loader.nil? || !loader.respond_to?(:memberless_project_class?)
1549
+
1550
+ loader.memberless_project_class?(class_name) && open_source_class?(class_name, scope, loader)
1551
+ end
1552
+
1553
+ OPENING_HOOKS = %i[inherited included extended prepended method_missing respond_to_missing?].freeze
1554
+ private_constant :OPENING_HOOKS
1555
+
1556
+ CLOSED_ROOTS = Set["Object", "BasicObject"].freeze
1557
+ private_constant :CLOSED_ROOTS
1558
+
1559
+ # Issue #2438 — whether a class's singleton surface may hold methods its source in `paths:` does not show,
1560
+ # read off `Scope::ResolutionChain` (`:constants` flavor, so a project module that holds only constants is
1561
+ # still a project entry). Open when the class, or a project class up its superclass chain:
1562
+ #
1563
+ # 1. `extend`s a module (any singleton-side entry beyond the classes' own singletons), or records an
1564
+ # `inherited` / `included` / `extended` / `prepended` hook or `method_missing` / `respond_to_missing?`.
1565
+ # A plain `include` or `prepend` adds instance methods only, so it leaves the singleton side closed;
1566
+ # 2. has no `class` / `module` body of its own inside `paths:` (`class Ext::Foo` names `Ext` without one);
1567
+ # 3. or the chain reaches a class the project does not define, other than `Object` / `BasicObject`: an
1568
+ # external superclass (`ApplicationRecord < ActiveRecord::Base`, `MyError < StandardError`), a
1569
+ # superclass written as an expression, or — where the source writes no superclass for the last class —
1570
+ # the superclass its member-less declaration writes, unless that is a project class.
1571
+ #
1572
+ # A definition or reopening in a file outside `paths:` cannot be seen, so a class whose body is inside
1573
+ # `paths:` and whose methods live only outside it counts as closed.
1574
+ def open_source_class?(class_name, scope, loader)
1575
+ name = class_name.to_s.delete_prefix("::")
1576
+ instance = Scope::ResolutionChain.for(scope, name, :instance, :constants)
1577
+ singleton = Scope::ResolutionChain.for(scope, name, :singleton, :constants)
1578
+ [instance, singleton].each { |chain| chain.record(scope) }
1579
+ classes = instance.level_classes.compact
1580
+ return true if classes.empty? || instance.truncated?
1581
+
1582
+ sources = scope.discovered_class_sources
1583
+ return true if classes.any? { |klass| (sources[klass] || []).empty? || opening_hook?(klass, scope) }
1584
+ return true if opening_entry?(instance, classes, mixins: false) ||
1585
+ opening_entry?(singleton, classes, mixins: true)
1586
+
1587
+ open_tail?(instance, classes.last, scope, loader)
1588
+ end
1589
+
1590
+ # An external superclass other than `Object` / `BasicObject` opens either side; a module entry opens the
1591
+ # chain only where `mixins` is set (the singleton side, where it is an `extend`).
1592
+ def opening_entry?(chain, classes, mixins:)
1593
+ chain.entries.any? do |entry|
1594
+ if entry.external? && entry.superclass_edge
1595
+ !CLOSED_ROOTS.include?(entry.raw.to_s.delete_prefix("::"))
1596
+ else
1597
+ mixins && !classes.include?(entry.name)
1598
+ end
1599
+ end
1600
+ end
1601
+
1602
+ def opening_hook?(klass, scope)
1603
+ OPENING_HOOKS.any? do |hook|
1604
+ scope.discovered_method?(klass, hook, :singleton) || scope.discovered_method?(klass, hook, :instance)
1605
+ end
1606
+ end
1607
+
1608
+ # The last class's own superclass, which the chain does not name: an expression the tables cannot resolve,
1609
+ # or one only the member-less declaration writes (the chain ends at the implicit `Object` exactly when
1610
+ # the source writes none).
1611
+ def open_tail?(chain, last, scope, loader)
1612
+ return true if chain.opaque_tail?(scope)
1613
+
1614
+ raw = loader.memberless_project_superclass(last)
1615
+ return false if raw.nil? || CLOSED_ROOTS.include?(raw.delete_prefix("::"))
1616
+
1617
+ !scope.discovered_classes.key?(raw.delete_prefix("::"))
1618
+ end
1619
+
1620
+ # Issue #2174 — `class Matrix` reopened with one `#:`-annotated method, and no `matrix` signatures
1621
+ # loaded, gives Matrix a declaration holding only that method ({RbsLoader#ruby_library_reopen?}).
1622
+ # `Matrix.identity(2)` is correct Ruby, so the class's surface is treated as open: `call.undefined-method`
1623
+ # declines on it, and a signature it only inherits (`Object#initialize` for `Matrix.new(rows)`) stops
1624
+ # deciding arity and argument types. Methods the project's own declaration lists stay checked.
1625
+ def ruby_library_reopen?(class_name, scope)
1626
+ loader = scope.environment&.rbs_loader
1627
+ return false if loader.nil? || !loader.respond_to?(:ruby_library_reopen?)
1628
+
1629
+ loader.ruby_library_reopen?(class_name)
1496
1630
  end
1497
1631
 
1498
1632
  def open_receiver?(class_name, scope)
@@ -1798,16 +1932,15 @@ module Rigor
1798
1932
  # Slice 7 phase 11 — wrong-arity diagnostic. Fires when
1799
1933
  # an explicit-receiver `Prism::CallNode` resolves to a
1800
1934
  # method whose declared overloads do not admit the
1801
- # supplied positional argument count. The rule applies
1802
- # ONLY to the simplest overload shape (single overload,
1803
- # no `rest_positionals`, no keyword parameters, no
1804
- # block-required positionals); calls with `*splat`
1805
- # arguments, keyword arguments, or block-pass arguments
1806
- # are silently skipped to avoid false positives. The
1807
- # check piggybacks on the same scope-index lookup used
1808
- # by `undefined_method_diagnostic`; it returns nil
1809
- # when the call's receiver / RBS coverage / call shape
1810
- # disqualifies the rule.
1935
+ # supplied positional argument count (across all
1936
+ # overloads, see {#compute_arity_envelope}). One overload
1937
+ # with required keyword arguments or trailing positionals
1938
+ # leaves the whole method unchecked; calls with `*splat`, keyword, or
1939
+ # block-pass arguments are silently skipped to avoid
1940
+ # false positives. The check piggybacks on the same
1941
+ # scope-index lookup used by `undefined_method_diagnostic`;
1942
+ # it returns nil when the call's receiver / RBS coverage /
1943
+ # call shape disqualifies the rule.
1811
1944
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1812
1945
  def wrong_arity_diagnostic(path, call_node, scope_index, lexical_sites = nil)
1813
1946
  return nil if call_node.receiver.nil?
@@ -2780,9 +2913,37 @@ module Rigor
2780
2913
 
2781
2914
  # Whether any project or `pre_eval:` file aliases `name`, on either side, which can make it another variable.
2782
2915
  def global_aliased?(scope, name)
2916
+ record_global_census(Inference::GlobalWriteCensus.alias_entry(name))
2783
2917
  Inference::GlobalWriteCensus.aliased?(global_write_census(scope), name)
2784
2918
  end
2785
2919
 
2920
+ # Issue #2158 — a read of the whole-program census records the `global_census:` key of each entry it asks
2921
+ # about ({Inference::GlobalWriteCensus.dependency_key}): a file that starts or stops writing the entry (a `def
2922
+ # write`, an `alias $stdout $x`, a computed `define_method`) re-checks the write. A `pre_eval:` file moves the
2923
+ # snapshot's own fingerprint.
2924
+ def record_global_census(*entries)
2925
+ return unless DependencyRecorder.active?
2926
+
2927
+ entries.each do |entry|
2928
+ DependencyRecorder.read_name(:global_census, Inference::GlobalWriteCensus.dependency_key(entry))
2929
+ end
2930
+ end
2931
+
2932
+ # Whether the census holds any of `entries`, recording only what can flip the answer: the entries present
2933
+ # when it holds one (the answer stays true until every one of them vanishes, and another appearing changes
2934
+ # nothing), every entry when it holds none.
2935
+ #
2936
+ # This rests on an invariant: every census entry can only move a write toward being silenced, and a reported
2937
+ # write is always re-checked through `IncrementalSession#global_write_reporters`, so recording only the
2938
+ # present entries on a yes is sound. A future census entry that makes a write reported would break it, and
2939
+ # its reader would have to record every entry. Recording every entry on a no is defence in depth, since the
2940
+ # write that answer leaves reported re-checks through `global_write_reporters` anyway.
2941
+ def census_holds_any?(census, entries)
2942
+ present = entries.select { |entry| census.include?(entry) }
2943
+ record_global_census(*(present.empty? ? entries : present))
2944
+ !present.empty?
2945
+ end
2946
+
2786
2947
  # A literal is rejected when its class is none the setter takes and, for a setter that also converts
2787
2948
  # (`to_str`, `to_int`) or asks for `write`, the literal's object cannot answer that method.
2788
2949
  def literal_rejected?(class_name, contract, node, scope, lexical_sites)
@@ -2830,11 +2991,15 @@ module Rigor
2830
2991
  end
2831
2992
 
2832
2993
  def program_may_answer?(ancestors, names, census, scope)
2833
- return true if Inference::GlobalWriteCensus.defines_any_of?(census, names)
2994
+ defines = [Inference::GlobalWriteCensus::DEFINES_ANY, *names.map { |name| [:defines, name] }]
2995
+ return true if census_holds_any?(census, defines)
2834
2996
 
2835
2997
  dynamic_mark = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
2836
2998
  return true if ancestors.any? { |owner| scope.parameter_envelopes_of(owner).key?(dynamic_mark) }
2837
2999
 
3000
+ # One key for every top-level mixin, a deliberate trade-off: the reader asks about each, so a mixin
3001
+ # appearing or vanishing anywhere re-checks every write that reached this step.
3002
+ record_global_census(%i[main_mixin *])
2838
3003
  main_mixins = Inference::GlobalWriteCensus.main_mixins(census)
2839
3004
  main_mixins.any? { |mixin| rbs_mixin_may_answer?(mixin, names, scope) } ||
2840
3005
  mixin_may_answer?(ancestors, names, scope)
@@ -2876,7 +3041,7 @@ module Rigor
2876
3041
  # active refinement on Ruby 4.0.5; a refined `respond_to_missing?` or `respond_to?`, and every implicit
2877
3042
  # conversion, do not, so a refinement that adds only those leaves the write reported.
2878
3043
  def refined_writer_in_effect?(census, node, scope, lexical_sites)
2879
- Inference::GlobalWriteCensus.refines_write?(census) &&
3044
+ census_holds_any?(census, [Inference::GlobalWriteCensus::REFINES_ANY, %i[refines write]]) &&
2880
3045
  (lexical_sites.nil? || !scope.declared_refinements.empty? ||
2881
3046
  lexical_sites.refinements.any_at?(node.location.start_offset))
2882
3047
  end
@@ -2911,8 +3076,7 @@ module Rigor
2911
3076
  # - Elsif chains (`subsequent` is itself an `IfNode`) ARE flagged — the entire downstream chain is
2912
3077
  # unreachable when the outer predicate is a constant literal.
2913
3078
  #
2914
- # Broadening to inferred-constant predicates is queued for a later v0.1.x release once the loop /
2915
- # mutation gaps named above are closed.
3079
+ # Inferred-constant predicates are the `flow.always-truthy-condition` rule's ({AlwaysTruthyConditionCollector}).
2916
3080
  def unreachable_branch_diagnostic(path, node, scope_index)
2917
3081
  scope = scope_index[node]
2918
3082
  return nil if scope.nil?
@@ -3023,7 +3187,7 @@ module Rigor
3023
3187
  build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
3024
3188
  end
3025
3189
 
3026
- PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
3190
+ PRIVATE_NAMES_MEMO = RunScopedSlots.register(:rigor_check_rules_private_singleton_names)
3027
3191
  private_constant :PRIVATE_NAMES_MEMO
3028
3192
 
3029
3193
  # Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
@@ -3032,6 +3196,7 @@ module Rigor
3032
3196
  # `Widget.new` would read, and file the dependency edges of, the whole singleton chain. The set is memoised per
3033
3197
  # discovery index.
3034
3198
  def private_singleton_candidate?(scope, name)
3199
+ DependencyRecorder.read_visibility("*", name) if DependencyRecorder.active?
3035
3200
  discovery = scope.discovery
3036
3201
  slot = Thread.current[PRIVATE_NAMES_MEMO]
3037
3202
  unless slot && slot[0].equal?(discovery)
@@ -3058,7 +3223,8 @@ module Rigor
3058
3223
  return true if rows.nil?
3059
3224
 
3060
3225
  offset = call_node.location.start_offset
3061
- rows.none? { |(start, finish, name, _kind, _owner)| !name.nil? && start <= offset && offset < finish }
3226
+ # A row with no side (a `def` in a block, a `define_method` name) is a block's range or no range at all.
3227
+ rows.none? { |(start, finish, _name, kind, _owner)| !kind.nil? && start <= offset && offset < finish }
3062
3228
  end
3063
3229
 
3064
3230
  SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
@@ -3070,6 +3236,7 @@ module Rigor
3070
3236
  discovery = scope.discovery
3071
3237
  chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
3072
3238
  chain.entries.any? do |entry|
3239
+ DependencyRecorder.read_visibility(entry.name, SINGLETON_WILDCARD_SLOT.last) unless entry.external?
3073
3240
  !entry.external? && discovery.contested?(:discovered_method_visibilities,
3074
3241
  [entry.name, SINGLETON_WILDCARD_SLOT])
3075
3242
  end
@@ -3299,12 +3466,9 @@ module Rigor
3299
3466
  # - Receiver must be Nominal / Singleton / Constant
3300
3467
  # (the same `concrete_class_name` test).
3301
3468
  # - Method must be in RBS.
3302
- # - Method must have exactly ONE method type
3303
- # (overload). Multi-overload checking is left for
3304
- # a follow-up because picking the "intended"
3305
- # overload requires the dispatcher's full
3306
- # acceptance plumbing.
3307
- # - The selected overload must have NO
3469
+ # - Overloads: a single method type is checked exactly; with several, an argument is a mismatch only
3470
+ # when every overload rejects it ({#multi_overload_argument_mismatch}).
3471
+ # - The overload checked must have NO
3308
3472
  # rest_positionals and NO trailing positionals —
3309
3473
  # the two shapes under which argument N no longer
3310
3474
  # binds to positional parameter N. Keyword
@@ -4014,16 +4178,14 @@ module Rigor
4014
4178
  # OWN declared return, and the sibling override rules gate on `defined_on?` — this one did not.
4015
4179
  # The inherited-declaration signal belongs to the ADR-35 family, which requires both sides
4016
4180
  # authored. Decided in ADR-110 WD3.
4017
- # - Skips methods whose enclosing class isn't a `Type::Singleton` self_type that we can name
4018
- # (top-level / module-level methods land outside the rule).
4181
+ # - Skips methods whose enclosing `self` names no class (top-level methods land outside the rule).
4019
4182
  # - Skips methods whose body's last expression is absent or types as `Dynamic[top]` (the
4020
4183
  # analyzer's fail-soft fallback) — emitting on `Dynamic[top]` would be noise.
4021
4184
  # - Compares the inferred body type against the declared return via `accepts?`:
4022
4185
  # :yes → silent
4023
4186
  # :no → emit at :error (severity_profile may re-stamp; default `balanced` keeps the
4024
4187
  # authored severity).
4025
- # :maybe → emit at :warning. Promoted to :error under `severity_profile: strict` per ADR-8 §
4026
- # "Severity profile".
4188
+ # :maybe → silent (see {#compare_return}; ADR-8's `strict` promotion is deferred).
4027
4189
  def return_type_mismatch_diagnostic(path, def_node, scope_index)
4028
4190
  return nil if def_node.body.nil?
4029
4191
 
@@ -4159,9 +4321,9 @@ module Rigor
4159
4321
  # precision improvements.
4160
4322
  def compare_return(declared, inferred)
4161
4323
  result = declared.accepts(inferred)
4162
- return :error if result.no?
4324
+ return nil unless result.no?
4163
4325
 
4164
- nil
4326
+ :error
4165
4327
  end
4166
4328
 
4167
4329
  def build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
@@ -4202,6 +4364,9 @@ module Rigor
4202
4364
 
4203
4365
  override_visibility = scope.discovered_method_visibility(class_name, method_name)
4204
4366
  return nil if override_visibility.nil?
4367
+ # Issue #2013 — a contested slot (a later `define_method` or alias gave the name another visibility, or a
4368
+ # write the walk cannot place) proves no reduction.
4369
+ return nil if scope.discovery.contested?(:discovered_method_visibilities, [class_name, method_name])
4205
4370
 
4206
4371
  parent = nearest_ancestor_visibility(scope, class_name, method_name)
4207
4372
  return nil if parent.nil?
@@ -149,8 +149,8 @@ module Rigor
149
149
 
150
150
  # True when `diagnostic` reports a failure inside Rigor that left the run readable but invalid as a
151
151
  # measurement of Rigor — see {ANALYZER_DEFECT_RULES}. Orthogonal to {.discards_file_analysis?}: the
152
- # user-facing tier reads the run; the Rigor-measuring tier should refuse it (the mutation fuzz does,
153
- # the ADR-69 kill oracle does not yet — #790).
152
+ # user-facing tier reads the run; the Rigor-measuring tier should refuse it (the mutation fuzz and the
153
+ # ADR-69 kill oracle both do, #790).
154
154
  def analyzer_defect?(diagnostic)
155
155
  ANALYZER_DEFECT_RULES.include?(diagnostic.rule)
156
156
  end
@@ -74,8 +74,8 @@ module Rigor
74
74
  @sources = Set.new
75
75
  @missing = Set.new
76
76
  # ADR-46 slice 4 — symbol-granularity tracking.
77
- # `symbol_sources`: source_path → Set<"ClassName#method"> for method-call deps.
78
- # `ancestry_sources`: Set<source_path> for class-ancestry (superclass / include) deps —
77
+ # `symbol_sources`: source_path → the "ClassName#method" names read from it, for method-call deps.
78
+ # `ancestry_sources`: the source paths read for class-ancestry (superclass / include) deps —
79
79
  # file-granularity by nature (a superclass edge touches the whole class).
80
80
  @symbol_sources = Hash.new { |h, k| h[k] = Set.new }
81
81
  @ancestry_sources = Set.new
@@ -103,8 +103,8 @@ module Rigor
103
103
  end
104
104
 
105
105
  # Frozen record of one file's cross-file reads.
106
- # `symbol_sources`: source_path → frozen Set<"ClassName#method"> (method-call edges).
107
- # `ancestry_sources`: frozen Set<source_path> (class-ancestry edges, file-granularity).
106
+ # `symbol_sources`: source_path → frozen set of "ClassName#method" names (method-call edges).
107
+ # `ancestry_sources`: frozen set of source paths (class-ancestry edges, file-granularity).
108
108
  Record = Data.define(:consumer, :sources, :missing, :symbol_sources, :ancestry_sources)
109
109
 
110
110
  # ADR-84 WD2 — a replayable, consumer-independent read-set: `reads` is a frozen Set of frozen
@@ -406,6 +406,17 @@ module Rigor
406
406
  end
407
407
  end
408
408
 
409
+ # Issue #2134 — {read_name} for a read of `class_name`'s visibility slots for `method_name`, on either side, or
410
+ # of their contest: `visibility:<class>#<method>`. `IncrementalSession#visibility_name_affected` fires the key
411
+ # when a file's visibility entry or contest for the slot moves. A reader that asks whether ANY class records
412
+ # the name private passes `"*"` as the class; the key fires when any entry of the name moves. A wildcard
413
+ # slot (`[:singleton, :*]`) is `*` as the method name.
414
+ def read_visibility(class_name, method_name)
415
+ return if Thread.current[KEY].nil?
416
+
417
+ read_name(:visibility, "#{class_name}##{method_name}")
418
+ end
419
+
409
420
  # The miss-only spelling of {read_name}: a cross-file lookup of `name` (kind `:method` / `:class` /
410
421
  # `:toplevel` / …) that resolved to nothing. Kept as the name the internal spec's negative-edge
411
422
  # contract uses.
@@ -25,9 +25,9 @@ module Rigor
25
25
  @mutex = Mutex.new
26
26
  end
27
27
 
28
- # @return frozen snapshot of the recorded boundary-cross events. Each entry is a Data
29
- # with `class_name` (String), `method_name` (Symbol), `gem_name` (String), and `rbs_display` (String
30
- # — the authoritative-side type's human-facing form, embedded into the diagnostic message).
28
+ # @return frozen snapshot of the recorded boundary-cross events. Each entry is a Data with `class_name`,
29
+ # `method_name`, `gem_name`, and `rbs_display` (the authoritative-side type's human-facing form, embedded
30
+ # into the diagnostic message).
31
31
  def entries
32
32
  @mutex.synchronize { @entries.dup.freeze }
33
33
  end
@@ -50,6 +50,17 @@ module Rigor
50
50
  end
51
51
  end
52
52
 
53
+ # The pool drain ({Rigor::Analysis::WorkerSession#drain_reporters}): the frozen entries.
54
+ def snapshot
55
+ entries
56
+ end
57
+
58
+ # Replays one worker's {#snapshot}; the record-time dedup collapses every worker's copy. Nil records
59
+ # nothing.
60
+ def absorb(snapshot)
61
+ Array(snapshot).each { |entry| record(**entry.to_h) }
62
+ end
63
+
53
64
  private
54
65
 
55
66
  def same_key?(existing, entry)