rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -178,7 +178,7 @@ module Rigor
178
178
 
179
179
  # Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
180
180
  # `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
181
- # wildcard meaning "every rule"), or for unknown tokens.
181
+ # wildcard meaning "every rule"), and an empty array for an unknown token.
182
182
  def self.resolve_rule_token(token)
183
183
  return nil if token == "all"
184
184
  return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
@@ -10,7 +10,7 @@ module Rigor
10
10
  module CheckRules
11
11
  # ADR-53 Track B — the engine-owned AST walk that hosts CheckRules' per-node collectors, so a file is
12
12
  # traversed once for all of them instead of once per collector (the ADR-52 WD4 model applied to the
13
- # built-in rules; this walk and {Plugin::NodeRuleWalk} converge in slice B4).
13
+ # built-in rules; {Plugin::NodeRuleWalk} drives the same collectors, ADR-53 B4).
14
14
  #
15
15
  # The walk is a single visit-before-descend DFS over `compact_child_nodes`. It threads the union of the
16
16
  # context the hosted collectors need — the per-collector hand-rolled walks each tracked some subset of
@@ -146,12 +146,14 @@ module Rigor
146
146
  qualified_prefix = extend_prefix(node, context.qualified_prefix)
147
147
  detached_ivar_facet = context.detached_ivar_facet || detached_ivar_facet?(node)
148
148
 
149
- Context.new(
150
- in_loop_or_block: in_loop_or_block,
151
- qualified_prefix: qualified_prefix,
152
- inside_def: inside_def,
153
- detached_ivar_facet: detached_ivar_facet
154
- )
149
+ # Nearly every node leaves all four fields as they were, and a {Context} is immutable, so the parent's
150
+ # is the child's. This runs once per node and was a top allocation site (a Data per node).
151
+ if in_loop_or_block == context.in_loop_or_block && inside_def == context.inside_def &&
152
+ qualified_prefix.equal?(context.qualified_prefix) && detached_ivar_facet == context.detached_ivar_facet
153
+ return context
154
+ end
155
+
156
+ Context.new(in_loop_or_block, qualified_prefix, inside_def, detached_ivar_facet)
155
157
  end
156
158
 
157
159
  # True for a node whose descendants write ivars to a store the enclosing class name does not name:
@@ -123,10 +123,9 @@ module Rigor
123
123
  @scope_index[node] || @scope_index[node.statements] || @scope_index[node.rescue_clause]
124
124
  end
125
125
 
126
- # Resolves one clause to `{clause:, names: [String, …]}` where `names` are the certified exception
127
- # CLASS names it rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat /
128
- # module — the comparisons involving it stay silent). A bare `rescue` names the implicit
129
- # `StandardError`.
126
+ # Resolves one clause to `{clause:, names:}`, where `names` are the certified exception CLASS names it
127
+ # rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat / module — the
128
+ # comparisons involving it stay silent). A bare `rescue` names the implicit `StandardError`.
130
129
  def clause_info(clause, prefix, scope)
131
130
  exceptions = clause.exceptions
132
131
  if exceptions.empty?
@@ -9,9 +9,9 @@ module Rigor
9
9
  # the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
10
10
  #
11
11
  # The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
12
- # `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
13
- # declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
14
- # that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
12
+ # `$;` is declared as a Regexp, a String or nil, but its setter also takes any object with `to_str`, and
13
+ # `$stdout` is declared as an IO, but its setter takes anything that responds to `write`. Each entry names
14
+ # the CRuby function that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
15
15
  #
16
16
  # A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
17
17
  # (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
@@ -280,8 +280,8 @@ module Rigor
280
280
 
281
281
  # The trailing `else` is dead when every subject value is covered by the `when` clauses — the final
282
282
  # falsey scope (recorded on the `else` node) narrows the subject to `bot`. Defensive `else` bodies (a
283
- # bare `raise` / `fail` / `throw`) are deliberate guards, not removable dead code, so they are
284
- # skipped.
283
+ # bare `raise` / `fail` / `throw` / `abort` / `exit`) are deliberate guards, not removable dead code, so
284
+ # they are skipped.
285
285
  def collect_else(else_clause, subject_name, keyword)
286
286
  return if else_clause.nil? || else_clause.statements.nil?
287
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,
@@ -33,6 +33,7 @@ require_relative "check_rules/rebinding_block_callees"
33
33
  require_relative "check_rules/rbs_receiver_surface"
34
34
  require_relative "check_rules/special_global_setters"
35
35
  require_relative "check_rules/ruby_deprecations"
36
+ require_relative "../run_scoped_slots"
36
37
 
37
38
  module Rigor
38
39
  module Analysis
@@ -53,7 +54,7 @@ module Rigor
53
54
  #
54
55
  # - implicit-self calls (no `node.receiver`) — too noisy
55
56
  # without per-method RBS for every helper in the class.
56
- # - dynamic / unknown receivers (`Dynamic[T]`, `Top`, `Union`)
57
+ # - dynamic / unknown receivers (`Dynamic[T]`, `Top`)
57
58
  # — by definition we cannot enumerate the method set.
58
59
  # - shape carriers: `Tuple` → "Array", `HashShape` → "Hash",
59
60
  # `Constant` → the constant's class — `concrete_class_name`
@@ -86,9 +87,8 @@ module Rigor
86
87
 
87
88
  module_function
88
89
 
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
90
+ # Returns the diagnostics for `root`'s subtree across the rule
91
+ # catalogue. The caller MUST have already produced `scope_index` through
92
92
  # `Rigor::Inference::ScopeIndexer.index(root, default_scope:)`.
93
93
  #
94
94
  # @param path — used to populate
@@ -750,7 +750,7 @@ module Rigor
750
750
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
751
751
  # gate belongs where that one consult is rather than duplicated into each shape's path.
752
752
  #
753
- # 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
754
754
  # shape needs a non-nil union of at least two RBS-DECLARED classes — an undeclared arm is an
755
755
  # ADR-26 open receiver and `union_arm_blocks_undefined_fire?` bails before the method lookup,
756
756
  # which is why the earlier probes read silent and were misread as the gate being unreachable.
@@ -797,13 +797,6 @@ module Rigor
797
797
  return union_undefined_method_diagnostic(path, call_node, receiver_type, scope, lexical_sites)
798
798
  end
799
799
 
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
800
  # An unbounded receiver surface: either a plugin-declared
808
801
  # open receiver (ADR-26 — e.g. `ActiveRecord::Relation`), or
809
802
  # a type Rigor synthesized (a missing-namespace module / a
@@ -839,6 +832,7 @@ module Rigor
839
832
  return nil if method_def
840
833
 
841
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)
842
836
  return nil if subclass_instance_may_answer?(rendered_receiver, class_name, call_node.name, scope,
843
837
  call_node.receiver)
844
838
 
@@ -1342,14 +1336,14 @@ module Rigor
1342
1336
  # (`METACLASS_ARMS`, "a `plugin_class : Class` really holds a `Plugin` subclass with `.manifest`");
1343
1337
  # the scalar rule enumerated `Module`'s own RBS instead and reported them.
1344
1338
  #
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.
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.
1347
1341
  #
1348
1342
  # Issue #879 supplied it for exactly one — `call.wrong-arity`, which decided `v.hexdigest(1, 2, 3)`
1349
1343
  # against `Digest::Instance`'s declaration on the same run where `v.no_such_method_zzz` was declined
1350
1344
  # as unenumerable. One receiver, one run, two answers about whether that surface can be read. The
1351
1345
  # 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
1346
+ # and `Class`-typed `.new` with a wrong arity is silent too, so the other callers stay on
1353
1347
  # `unbounded_receiver_surface?` until something demonstrates otherwise.
1354
1348
  #
1355
1349
  # Arity takes {#unenumerable_receiver_shape?}, not this whole predicate: ADR-26's open receivers are
@@ -1451,14 +1445,16 @@ module Rigor
1451
1445
  end
1452
1446
 
1453
1447
  # 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.
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.
1455
1451
  def inferred_param_argument?(arg, scope)
1456
- inferred_param_rooted?(arg, scope)
1452
+ inferred_param_rooted?(arg, scope, fold: false)
1457
1453
  end
1458
1454
 
1459
1455
  # ADR-67 WD6b — see {CheckRules::InferredParamGuard} for the shared root-walk contract.
1460
- def inferred_param_rooted?(node, scope)
1461
- InferredParamGuard.rooted?(node, scope)
1456
+ def inferred_param_rooted?(node, scope, fold: true)
1457
+ InferredParamGuard.rooted?(node, scope, fold: fold)
1462
1458
  end
1463
1459
 
1464
1460
  def constant_class_name(value)
@@ -1530,6 +1526,97 @@ module Rigor
1530
1526
  ruby_library_reopen?(class_name, scope)
1531
1527
  end
1532
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
+
1533
1620
  # Issue #2174 — `class Matrix` reopened with one `#:`-annotated method, and no `matrix` signatures
1534
1621
  # loaded, gives Matrix a declaration holding only that method ({RbsLoader#ruby_library_reopen?}).
1535
1622
  # `Matrix.identity(2)` is correct Ruby, so the class's surface is treated as open: `call.undefined-method`
@@ -1845,16 +1932,15 @@ module Rigor
1845
1932
  # Slice 7 phase 11 — wrong-arity diagnostic. Fires when
1846
1933
  # an explicit-receiver `Prism::CallNode` resolves to a
1847
1934
  # 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.
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.
1858
1944
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1859
1945
  def wrong_arity_diagnostic(path, call_node, scope_index, lexical_sites = nil)
1860
1946
  return nil if call_node.receiver.nil?
@@ -2990,8 +3076,7 @@ module Rigor
2990
3076
  # - Elsif chains (`subsequent` is itself an `IfNode`) ARE flagged — the entire downstream chain is
2991
3077
  # unreachable when the outer predicate is a constant literal.
2992
3078
  #
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.
3079
+ # Inferred-constant predicates are the `flow.always-truthy-condition` rule's ({AlwaysTruthyConditionCollector}).
2995
3080
  def unreachable_branch_diagnostic(path, node, scope_index)
2996
3081
  scope = scope_index[node]
2997
3082
  return nil if scope.nil?
@@ -3102,7 +3187,7 @@ module Rigor
3102
3187
  build_visibility_mismatch_diagnostic(path, call_node, "singleton(#{class_name})")
3103
3188
  end
3104
3189
 
3105
- PRIVATE_NAMES_MEMO = :rigor_check_rules_private_singleton_names
3190
+ PRIVATE_NAMES_MEMO = RunScopedSlots.register(:rigor_check_rules_private_singleton_names)
3106
3191
  private_constant :PRIVATE_NAMES_MEMO
3107
3192
 
3108
3193
  # Issue #1984 — the table-only gate the singleton side asks before it reads a chain, as the instance side asks
@@ -3381,12 +3466,9 @@ module Rigor
3381
3466
  # - Receiver must be Nominal / Singleton / Constant
3382
3467
  # (the same `concrete_class_name` test).
3383
3468
  # - 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
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
3390
3472
  # rest_positionals and NO trailing positionals —
3391
3473
  # the two shapes under which argument N no longer
3392
3474
  # binds to positional parameter N. Keyword
@@ -4096,16 +4178,14 @@ module Rigor
4096
4178
  # OWN declared return, and the sibling override rules gate on `defined_on?` — this one did not.
4097
4179
  # The inherited-declaration signal belongs to the ADR-35 family, which requires both sides
4098
4180
  # 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).
4181
+ # - Skips methods whose enclosing `self` names no class (top-level methods land outside the rule).
4101
4182
  # - Skips methods whose body's last expression is absent or types as `Dynamic[top]` (the
4102
4183
  # analyzer's fail-soft fallback) — emitting on `Dynamic[top]` would be noise.
4103
4184
  # - Compares the inferred body type against the declared return via `accepts?`:
4104
4185
  # :yes → silent
4105
4186
  # :no → emit at :error (severity_profile may re-stamp; default `balanced` keeps the
4106
4187
  # authored severity).
4107
- # :maybe → emit at :warning. Promoted to :error under `severity_profile: strict` per ADR-8 §
4108
- # "Severity profile".
4188
+ # :maybe → silent (see {#compare_return}; ADR-8's `strict` promotion is deferred).
4109
4189
  def return_type_mismatch_diagnostic(path, def_node, scope_index)
4110
4190
  return nil if def_node.body.nil?
4111
4191
 
@@ -4241,9 +4321,9 @@ module Rigor
4241
4321
  # precision improvements.
4242
4322
  def compare_return(declared, inferred)
4243
4323
  result = declared.accepts(inferred)
4244
- return :error if result.no?
4324
+ return nil unless result.no?
4245
4325
 
4246
- nil
4326
+ :error
4247
4327
  end
4248
4328
 
4249
4329
  def build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
@@ -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
@@ -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)
@@ -44,7 +44,7 @@ module Rigor
44
44
  Resolved.new(
45
45
  gem_name: entry.gem,
46
46
  version: spec.version.to_s,
47
- gem_dir: spec.full_gem_path, # rigor:disable undefined-method
47
+ gem_dir: spec.full_gem_path,
48
48
  mode: entry.mode,
49
49
  roots: entry.roots
50
50
  )
@@ -15,10 +15,9 @@ module Rigor
15
15
  :class_to_gem, :budget_overrun_strategy, :gem_modes, :refinements, :bound_constants,
16
16
  :bound_segments
17
17
 
18
- # @param method_catalog — => Symbol}] the flat
19
- # `(class_name, method_name) → :instance | :singleton` table produced by {Walker.walk}, aggregated
20
- # across every resolved gem in the run. The Index itself stays gem-agnostic — the per-gem
21
- # attribution that slice 3's cache descriptor needs lives on `Resolved`, not here.
18
+ # @param method_catalog — the flat `(class_name, method_name) → CatalogEntry` table produced by
19
+ # {Walker.walk}, aggregated across every resolved gem in the run. The Index itself stays gem-agnostic —
20
+ # the per-gem attribution that slice 3's cache descriptor needs lives on `Resolved`, not here.
22
21
  # @param budget_exceeded — gem names whose {Walker} run hit the per-gem catalog cap
23
22
  # (slice 4). The Runner consumes this list to emit one `dynamic.dependency-source.budget-exceeded`
24
23
  # warning per gem.
@@ -110,10 +109,9 @@ module Rigor
110
109
  mode_for(class_name) == :full
111
110
  end
112
111
 
113
- # Looks up the recorded method kind for a `(class_name, method_name)` pair. Returns `:instance` /
114
- # `:singleton` when the walker observed a definition under one of the resolved gems' `roots:`, or
115
- # `nil` otherwise. Slice 2b-ii enriches this with the inferred return type so the dispatcher tier can
116
- # build a `Type::Dynamic` directly from the lookup result.
112
+ # Looks up the `CatalogEntry` (kind, and the heuristic return type) for a `(class_name, method_name)` pair.
113
+ # Returns it when the walker observed a definition under one of the resolved gems' `roots:`, or `nil`
114
+ # otherwise.
117
115
  def contribution_for(class_name:, method_name:)
118
116
  @method_catalog[[class_name, method_name]]
119
117
  end
@@ -99,10 +99,10 @@ module Rigor
99
99
  # `Configuration::Dependencies::Entry`).
100
100
  # @param budget — per-gem catalog cap (method-definition count). When unset, defaults
101
101
  # to `UNBOUNDED` for backwards-compatible test paths.
102
- # @return frozen wrapper carrying the catalog (`Hash{[class_name, method_name] =>
103
- # :instance | :singleton}`) and a `truncated?` flag set when the walker stopped harvesting because
104
- # the budget was reached. Methods of identical name on the same class with different kinds (rare;
105
- # private API mostly) carry the kind that wins the per-class first walk.
102
+ # @return frozen wrapper carrying the catalog (keyed by `[class_name, method_name]`, valued by
103
+ # {CatalogEntry}) and a `truncated?` flag set when the walker stopped harvesting because the budget was
104
+ # reached. Methods of identical name on the same class with different kinds (rare; private API mostly)
105
+ # carry the kind that wins the per-class first walk.
106
106
  def walk(gem_dir:, roots:, budget: UNBOUNDED)
107
107
  harvest = Harvest.new({}, {}, nil, nil, Set.new, Set.new)
108
108
  truncated = false
@@ -15,22 +15,108 @@ module Rigor
15
15
  # into the closure. Emptying alone does not exercise that one: the broader key it fires re-checks the reader
16
16
  # anyway. Not every finer edge is reached: dropping a body still moves a broader key for some of them. A
17
17
  # `visibility:` edge is one (#2161's `module_function :two` to `private :two` swap passes both edits with the
18
- # edge removed), and only an edit that changes a body while keeping it (#2277) would test it.
18
+ # edge removed), and only an edit that changes a body while keeping it would test it.
19
+ # - `dropped` (#2277) — the file with ONE statement of a `class` / `module` body removed and everything else kept,
20
+ # so the declarations, the other definitions and every broader key stay as they were and a finer edge is the
21
+ # only thing that re-checks a reader. At most {MAX_DROPS} statements per file: the visibility-changing calls
22
+ # (`private`, `module_function`, ... {VISIBILITY_CALLS}) first, a body's own statements before those of the bodies
23
+ # nested in it, since those are what finer edges such as #2161's `visibility:` read; then the other statements,
24
+ # spread evenly over the file. A drop whose remainder does not parse (a heredoc body left behind) gives way to the
25
+ # next candidate. Statements of a `class << self` body are never candidates.
19
26
  #
20
- # Neither replays an addition or a body-changing edit. A skeleton identical to the file, or with no declaration
21
- # (the emptied file again, or a file that does not parse), is not replayed.
27
+ # No edit is an addition. An edit identical to the file or to an earlier edit, or a file that does not parse
28
+ # (or, for the skeleton, declares nothing), is not replayed.
22
29
  module EditReplay
23
30
  Edit = Data.define(:label, :source)
24
31
 
32
+ # The most body-statement drops replayed per file; each is a recheck plus a full analysis.
33
+ MAX_DROPS = 2
34
+
35
+ VISIBILITY_CALLS = %i[
36
+ private public protected module_function private_class_method public_class_method
37
+ private_constant public_constant
38
+ ].freeze
39
+
25
40
  module_function
26
41
 
27
42
  # The edits to replay for `source`, in order.
28
43
  def edits(source)
29
44
  emptied = Edit.new(label: "emptying", source: "")
30
45
  reduced = skeleton(source)
31
- return [emptied] if reduced.empty? || reduced == source
46
+ edits = [emptied]
47
+ edits << Edit.new(label: "reducing to its class/module skeleton", source: reduced) unless reduced.empty?
48
+ edits.concat(drops(source))
49
+ edits.reject { |edit| edit.source == source }.uniq(&:source)
50
+ end
51
+
52
+ # Up to {MAX_DROPS} edits, each `source` less one statement of a class / module body, in source order. A drop that
53
+ # leaves text that does not parse is never returned: the next candidate takes its place.
54
+ def drops(source)
55
+ result = Prism.parse(source)
56
+ return [] unless result.success?
57
+
58
+ picked = []
59
+ preferred(drop_candidates(result.value.statements.body)).each do |node|
60
+ edit = drop_edit(source, node)
61
+ picked << [node, edit] if edit
62
+ break if picked.size == MAX_DROPS
63
+ end
64
+ picked.sort_by { |node, _| node.location.start_offset }.map(&:last)
65
+ end
66
+
67
+ # `source` less `node`, or nil when what is left does not parse. The range runs to the furthest heredoc body
68
+ # inside the node, which sits after the node's own location.
69
+ def drop_edit(source, node)
70
+ location = node.location
71
+ from, to = whole_lines(source, location.start_offset, [location.end_offset, heredoc_end(node)].max)
72
+ dropped = source.byteslice(0, from) + source.byteslice(to..)
73
+ return nil unless Prism.parse(dropped).success?
74
+
75
+ Edit.new(label: "dropping the statement at line #{location.start_line} of", source: dropped)
76
+ end
77
+
78
+ # The end offset of the furthest heredoc terminator inside `node` (0 when it holds none).
79
+ def heredoc_end(node)
80
+ own = node.respond_to?(:closing_loc) && node.respond_to?(:opening_loc) &&
81
+ node.opening_loc&.slice&.start_with?("<<")
82
+ ends = node.compact_child_nodes.map { |child| heredoc_end(child) }
83
+ ends << node.closing_loc.end_offset if own && node.closing_loc
84
+ ends.max || 0
85
+ end
86
+
87
+ # The byte range of `start...finish` widened to the lines it alone occupies, so no blank line is left behind.
88
+ def whole_lines(source, start, finish)
89
+ head = source.byteslice(0, start)[/(?<=\A|\n)[ \t]*\z/]
90
+ tail = finish.positive? && source.getbyte(finish - 1) == 10 ? "" : source.byteslice(finish..)[/\A[ \t]*\n/]
91
+ head.nil? || tail.nil? ? [start, finish] : [start - head.bytesize, finish + tail.bytesize]
92
+ end
93
+
94
+ # Every statement directly in a `class` / `module` body of `nodes`, nested bodies included, in the order a body's
95
+ # own statements come before those of the bodies nested in it (a nested declaration is itself a candidate,
96
+ # dropped whole). A `class << self` body is never searched: its statements are not candidates.
97
+ def drop_candidates(nodes)
98
+ nodes.flat_map do |node|
99
+ next [] unless (node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)) &&
100
+ node.body.is_a?(Prism::StatementsNode)
101
+
102
+ body = node.body.body
103
+ [*body, *drop_candidates(body)]
104
+ end
105
+ end
106
+
107
+ # The candidates in the order to try them: the visibility calls, then the other statements from the evenly spread
108
+ # picks outward, then the rest of the other statements.
109
+ def preferred(candidates)
110
+ visibility, others = candidates.partition { |node| visibility_call?(node) }
111
+ spread = Array.new(others.empty? ? 0 : MAX_DROPS) do |index|
112
+ others[(((index * 2) + 1) * others.size) / (MAX_DROPS * 2)]
113
+ end
114
+ [*visibility, *spread, *others].uniq(&:object_id)
115
+ end
32
116
 
33
- [emptied, Edit.new(label: "reducing to its class/module skeleton", source: reduced)]
117
+ def visibility_call?(node)
118
+ node.is_a?(Prism::CallNode) && VISIBILITY_CALLS.include?(node.name) &&
119
+ (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
34
120
  end
35
121
 
36
122
  # `source`'s `class` / `module` declarations, nested, with their bodies dropped; "" when it does not parse or
@@ -61,7 +147,8 @@ module Rigor
61
147
  superclass = node.superclass
62
148
  superclass ? "class #{node.constant_path.slice} < #{superclass.slice}" : "class #{node.constant_path.slice}"
63
149
  end
64
- private_class_method :declarations, :header_of
150
+ private_class_method :declarations, :header_of, :drop_candidates, :whole_lines, :drop_edit, :heredoc_end,
151
+ :preferred, :visibility_call?
65
152
  end
66
153
  end
67
154
  end