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
data/lib/rigor/scope.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "type"
4
4
  require_relative "environment"
5
5
  require_relative "scope/discovery_index"
6
+ require_relative "run_scoped_slots"
6
7
  require_relative "scope/resolution_chain"
7
8
  require_relative "inference/definer_resolution"
8
9
  require_relative "inference/in_effect_refinements"
@@ -13,6 +14,9 @@ require_relative "inference/flow_tracer"
13
14
  require_relative "inference/optimistic_origin"
14
15
  require_relative "inference/statement_evaluator"
15
16
  require_relative "inference/def_node_resolver"
17
+ # Issue #2608 — read by `MutationWidening` at a remover call, which only an analysis makes; loaded with the engine
18
+ # so the engine-free value layer that requires `MutationWidening` stays as small as it was.
19
+ require_relative "inference/remover_narrowing"
16
20
 
17
21
  module Rigor
18
22
  # Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State
@@ -30,7 +34,7 @@ module Rigor
30
34
  :opaque_block_self, :block_self_unknown, :singleton_class_body, :lexical_nesting,
31
35
  :dynamic_origins, :local_origins, :ivar_origins,
32
36
  :void_origins, :plugin_typed_calls,
33
- :optimistic_origins, :optimistic_locals, :optimistic_ivars,
37
+ :optimistic_origins, :optimistic_locals, :optimistic_ivars, :construction_facts,
34
38
  :repeated_or_writes, :match_frame,
35
39
  :constant_narrowings, :guard_records, :bot_guard_classes, :declared_refinements
36
40
 
@@ -89,7 +93,7 @@ module Rigor
89
93
  def discovered_prepends = @discovery.discovered_prepends.transform_values { |names| names.uniq.freeze }.freeze
90
94
  def discovered_extends = @discovery.discovered_extends
91
95
  def discovered_class_sources = @discovery.discovered_class_sources
92
- # Issue #644 — `{qualified constant name => Set[declaring file]}`; seeded only on an ADR-46 recording run.
96
+ # Issue #644 — `{qualified constant name => declaring files}`; seeded only on an ADR-46 recording run.
93
97
  def constant_sources = @discovery.constant_sources
94
98
  def published_constant_names = @discovery.published_constant_names
95
99
  def local_constant_names = @discovery.local_constant_names
@@ -202,9 +206,9 @@ module Rigor
202
206
  # `:const` allowed; its entry marks presence only.
203
207
  IndexedKey = Data.define(:receiver_kind, :receiver_name, :key)
204
208
 
205
- # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain per A1
206
- # from the ROADMAP § Future cycles slice). The value of the map at this key is the narrowed type the next read
207
- # of the same chain MUST observe — typically the post-`is_a?(C)` narrowing established on a predicate edge.
209
+ # Narrowing key for a no-arg / no-block method-call chain `receiver.method_name` (a "single-hop" chain).
210
+ # The value of the map at this key is the narrowed type the next read of the same chain MUST observe —
211
+ # typically the post-`is_a?(C)` narrowing established on a predicate edge.
208
212
  #
209
213
  # - `receiver_kind` ∈ `{:local, :ivar}` — the analyzer only tracks chains rooted at a local or instance
210
214
  # variable today (Law-of-Demeter-style single-hop).
@@ -232,7 +236,7 @@ module Rigor
232
236
  # through flow transitions and ignored by `==` / `hash`.
233
237
  EMPTY_FOLD_SAFE = Set.new.freeze
234
238
  # ADR-82 WD1 — provenance propagation across the receiver-node lookup. `local_origins` / `ivar_origins`
235
- # map a `name` (Symbol) to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
239
+ # map a `name` to the {Inference::DynamicOrigin} cause of the `Dynamic` value currently bound to
236
240
  # it, so a downstream dispatch whose receiver is a bare `x` / `@x` read resolves to why that value is
237
241
  # dynamic (the cause was recorded on the *assignment*'s rhs node, which the receiver-read node is not).
238
242
  # Advisory metadata like {#dynamic_origins}: ignored by `==` / `hash` (never varies a flow decision) and
@@ -322,6 +326,8 @@ module Rigor
322
326
  @plugin_typed_calls.key?(node)
323
327
  end
324
328
 
329
+ # Every keyword past the 31st takes CRuby's allocating unspecified-keywords path when omitted with a non-literal
330
+ # default; see the note at {#rebuild} before adding a field.
325
331
  def initialize( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- one keyword and assignment per field
326
332
  environment:, locals:,
327
333
  fact_store: Analysis::FactStore.empty,
@@ -348,6 +354,7 @@ module Rigor
348
354
  optimistic_origins: {}.compare_by_identity,
349
355
  optimistic_locals: EMPTY_ORIGINS,
350
356
  optimistic_ivars: EMPTY_ORIGINS,
357
+ construction_facts: EMPTY_ORIGINS,
351
358
  repeated_or_writes: EMPTY_REPEATED_OR_WRITES,
352
359
  match_frame: nil,
353
360
  constant_narrowings: EMPTY_CONSTANT_NARROWINGS,
@@ -381,6 +388,7 @@ module Rigor
381
388
  @optimistic_origins = optimistic_origins
382
389
  @optimistic_locals = optimistic_locals
383
390
  @optimistic_ivars = optimistic_ivars
391
+ @construction_facts = construction_facts
384
392
  @repeated_or_writes = repeated_or_writes
385
393
  @match_frame = match_frame
386
394
  @constant_narrowings = constant_narrowings
@@ -433,6 +441,41 @@ module Rigor
433
441
  rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => mark).freeze)
434
442
  end
435
443
 
444
+ # Issue #2555 L3 — the construction facts a LOCAL's binding carries: what the engine knows about how the object
445
+ # bound to it was made, as opposed to what its type says about its contents. `:synthesised` means the engine
446
+ # built the object itself (a collection literal, or an `Array.new` / `Hash.new` / `Set.new` / `X[...]`
447
+ # constructor on the core class), so its element types are this engine's own inference and no declaration's
448
+ # claim: the mutation seams may re-grow such a carrier precisely, where a declared one (a parameter, an RBS
449
+ # return) keeps the issue #561 boundary. The table is `{ name => frozen Set[Symbol] }`, so a second fact joins
450
+ # an existing entry without reshaping it.
451
+ #
452
+ # Locals only. An instance variable's binding is seeded across methods from the class-ivar index, whose
453
+ # construction no single body witnesses, so an ivar never carries a fact. Like {#optimistic_locals} the facts
454
+ # are kept by {#with_mutated_local} (the binding still names the same object), dropped by {#with_local} (a
455
+ # source-level rebind names a new one), intersected at a merge, and reset with the locals at a method body's
456
+ # entry. No fact is copied by an alias write (`b = a`): the type is shared, the provenance is not tracked
457
+ # through a second name (issue #2335), and a local that may escape anywhere in its body is never stamped
458
+ # ({Inference::ConstructionEscape}). Within an entry `:synthesised` intersects at a merge and `:grown` (a store
459
+ # has grown the carrier since construction) unions.
460
+ CONSTRUCTION_FACT_SETS = Ractor.make_shareable({ synthesised: Set[:synthesised] })
461
+ NO_CONSTRUCTION_FACTS = Set.new.freeze
462
+ private_constant :CONSTRUCTION_FACT_SETS, :NO_CONSTRUCTION_FACTS
463
+
464
+ def construction_fact?(name, fact = :synthesised)
465
+ @construction_facts.fetch(name.to_sym, NO_CONSTRUCTION_FACTS).include?(fact)
466
+ end
467
+
468
+ # Records `fact` on the local `name`. Applied AFTER the {#with_local} that binds the value, which drops any
469
+ # fact the old binding held.
470
+ def with_construction_fact(name, fact)
471
+ key = name.to_sym
472
+ existing = @construction_facts[key]
473
+ return self if existing&.include?(fact)
474
+
475
+ facts = existing ? (existing | [fact]).freeze : (CONSTRUCTION_FACT_SETS[fact] || Set[fact].freeze)
476
+ rebuild(@construction_facts.merge(key => facts).freeze)
477
+ end
478
+
436
479
  # ADR-82 WD1 — the propagated origin of the `Dynamic` value currently bound to a local / instance
437
480
  # variable, or `nil` when none is tracked. Consulted by `Inference::ProtectionScanner` when a dispatch's
438
481
  # receiver is a bare `x` / `@x` read whose own node carries no origin.
@@ -487,7 +530,8 @@ module Rigor
487
530
  # longer holds (the new value may carry a method-local nil). `with_declaration_sourced_local` re-establishes
488
531
  # the mark afterward when the RHS is a pure copy of a declaration-sourced ivar read; the default is to drop
489
532
  # it.
490
- rebuild(locals: new_locals, fact_store: new_fact_store,
533
+ rebuild(keep_marks ? @construction_facts : drop_origin(@construction_facts, name),
534
+ locals: new_locals, fact_store: new_fact_store,
491
535
  indexed_narrowings: new_indexed_narrowings,
492
536
  method_chain_narrowings: new_chain_narrowings,
493
537
  declaration_sourced: keep_marks ? @declaration_sourced : drop_local_declaration_marks(name),
@@ -681,9 +725,8 @@ module Rigor
681
725
 
682
726
  # Slice 7 phase 1 — instance/class/global variable bindings. `ivar(name)` / `cvar(name)` / `global(name)`
683
727
  # return the type currently bound for the named variable, or `nil` when the variable has not been written in
684
- # the analyzed slice of the program. The first cut tracks bindings only within a single method body (each
685
- # `def` enters with a fresh binding map), so reads in other methods of the same class fall through to
686
- # `Dynamic[Top]`. Cross-method ivar/cvar inference is a follow-up slice.
728
+ # the analyzed slice of the program. Each `def` enters with a fresh binding map seeded from the class-level
729
+ # accumulators ({#class_ivars_for}, {#class_cvars_for}).
687
730
  def ivar(name)
688
731
  @ivars[name.to_sym]
689
732
  end
@@ -1186,8 +1229,8 @@ module Rigor
1186
1229
  end
1187
1230
 
1188
1231
  # Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. `"Rigor::Scope"`);
1189
- # the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of
1190
- # that class. `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
1232
+ # the value maps every ivar that appears as a write target inside any def body of that class to its type.
1233
+ # `StatementEvaluator#build_method_entry_scope` seeds the method body's `ivars` map from this
1191
1234
  # table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
1192
1235
  #
1193
1236
  # `ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map
@@ -1207,7 +1250,7 @@ module Rigor
1207
1250
  @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS
1208
1251
  end
1209
1252
 
1210
- # Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a `Hash[Symbol, Symbol]` of
1253
+ # Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a table of
1211
1254
  # `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and
1212
1255
  # recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and
1213
1256
  # wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically,
@@ -1230,6 +1273,33 @@ module Rigor
1230
1273
  true
1231
1274
  end
1232
1275
 
1276
+ # Issue #2555 — whether the project defines a method named `method_name` anywhere: on any class or module,
1277
+ # instance or singleton side (the def-node tables, which hold a plain cross-file `def` on an RBS-declared class
1278
+ # that `discovered_methods` withholds, and `discovered_methods`), or in a `refine` block. The name set is built
1279
+ # once per discovery index. The answer depends on every file, so an incremental run records the name either way:
1280
+ # a file that starts or stops defining it re-analyses the reader (issue #1459's `defines:` edge).
1281
+ def project_method_name?(method_name)
1282
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1283
+ table = (Thread.current[PROJECT_METHOD_NAMES_KEY] ||= ObjectSpace::WeakKeyMap.new)
1284
+ return true if (table[@discovery] ||= collect_project_method_names).include?(method_name.to_sym)
1285
+
1286
+ false
1287
+ end
1288
+
1289
+ def collect_project_method_names
1290
+ names = Set.new
1291
+ [@discovery.discovered_methods, @discovery.discovered_def_nodes, @discovery.discovered_singleton_def_nodes,
1292
+ @discovery.discovered_refinements].each do |by_owner|
1293
+ by_owner.each_value { |methods| methods.each_key { |method_name| names << method_name.to_sym } }
1294
+ end
1295
+ names.freeze
1296
+ end
1297
+ private :collect_project_method_names
1298
+
1299
+ # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so an edited project is read afresh.
1300
+ PROJECT_METHOD_NAMES_KEY = RunScopedSlots.register(:__rigor_project_method_names)
1301
+ private_constant :PROJECT_METHOD_NAMES_KEY
1302
+
1233
1303
  def record_discovered_method(class_name, method_name)
1234
1304
  record_class_dependency(class_name)
1235
1305
  Analysis::DependencyRecorder.read_name(:defines, method_name)
@@ -1258,7 +1328,7 @@ module Rigor
1258
1328
  per_class && per_class[method_name]
1259
1329
  end
1260
1330
 
1261
- # v0.0.2 #5 — per-class table mapping `method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer`
1331
+ # v0.0.2 #5 — per-class table mapping `method_name → Prism::DefNode`. Populated by `ScopeIndexer`
1262
1332
  # alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced
1263
1333
  # methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do
1264
1334
  # inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
@@ -1272,6 +1342,86 @@ module Rigor
1272
1342
  Inference::DefNodeResolver.resolve(entry)
1273
1343
  end
1274
1344
 
1345
+ # Issue #2515 — `[class_name, open]` for each class or module whose instance def table holds `method_name`, in
1346
+ # table order (frozen; empty when no user def has the name). The callee floor's hand-off on a receiver it cannot
1347
+ # type reads every def of the name, so the answer depends on the whole project's defs of that name: a recorder
1348
+ # files the `defines:<name>` name edge, which fires when a file starts or stops defining it. `open` is true when
1349
+ # the real receiver may not be the class the def sits on: the owner is a module (an includer's methods are
1350
+ # called from it), or a project class names it as its superclass (a subclass may override what the def calls).
1351
+ # A recorder files `subclass_of:<owner>` for the second.
1352
+ def user_instance_def_owners(method_name)
1353
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1354
+ name = method_name.to_sym
1355
+ owners = nil
1356
+ @discovery.discovered_def_nodes.each do |class_name, defs|
1357
+ next unless defs.key?(name) && class_name != Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
1358
+
1359
+ (owners ||= []) << [class_name, open_to_override?(class_name)].freeze
1360
+ end
1361
+ owners.nil? ? EMPTY_OWNERS : owners.freeze
1362
+ end
1363
+
1364
+ EMPTY_OWNERS = [].freeze
1365
+ private_constant :EMPTY_OWNERS
1366
+
1367
+ # Issue #2515 — `[opaque, keys]`: whether the project holds a subclass relation the superclass table does not
1368
+ # spell as `class X < Name`, so that no owner can be called free of an override. It holds when a class takes
1369
+ # `Class.new(Parent)` ({DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}), when a class's superclass expression is
1370
+ # unreadable (`nil` in the table), or when a written superclass is not a declared class and some file assigns a
1371
+ # constant of that name (`H2 = Helper; class Sub < H2`; the writer census {#bound_constant_names} or this file's
1372
+ # constant table).
1373
+ # `keys` are the frozen ADR-46 name keys a recorder files for the answer; the caller files them, since the answer
1374
+ # is memoised per discovery.
1375
+ def subclassing_opacity
1376
+ keys = OPACITY_KEYS.dup
1377
+ opaque = @discovery.discovered_parameter_envelopes.each_value.any? do |bucket|
1378
+ bucket.key?(DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
1379
+ end
1380
+ segments = {}
1381
+ @discovery.discovered_superclasses.each_value do |parent|
1382
+ opaque = true if parent.nil? || alias_superclass?(parent, segments, keys)
1383
+ end
1384
+ [opaque ? true : false, keys.each(&:freeze).freeze].freeze
1385
+ end
1386
+
1387
+ # True when `parent`, a written superclass that is no declared class, names a constant some file assigns. Files
1388
+ # the `constant:<segment>` key for it once per segment.
1389
+ def alias_superclass?(parent, segments, keys)
1390
+ return false if @discovery.discovered_classes.key?(parent)
1391
+
1392
+ cut = parent.rindex("::")
1393
+ segment = cut ? parent.byteslice(cut + 2, parent.bytesize - cut - 2) : parent
1394
+ return false if segments.key?(segment)
1395
+
1396
+ segments[segment] = true
1397
+ keys << "constant:#{segment}"
1398
+ @discovery.constant_writers.key?(segment) || @discovery.in_source_constants.key?(parent)
1399
+ end
1400
+ private :alias_superclass?
1401
+
1402
+ OPACITY_KEYS = %w[subclass_of:* mark:*].freeze
1403
+ private_constant :OPACITY_KEYS
1404
+
1405
+ def open_to_override?(class_name)
1406
+ Analysis::DependencyRecorder.read_last_segment(:subclass_of, class_name) if Analysis::DependencyRecorder.active?
1407
+ return true if @discovery.discovered_parameter_envelopes[class_name]&.key?(DiscoveryIndex::ENVELOPE_MODULE_MARK)
1408
+
1409
+ @discovery.discovered_superclasses.any? do |child, parent|
1410
+ child != class_name && parent && names_class?(parent, class_name)
1411
+ end
1412
+ end
1413
+ private :open_to_override?
1414
+
1415
+ # True when a superclass written as `written` (`Helper`, `A::Helper`, `::Helper`) may name `qualified`.
1416
+ def names_class?(written, qualified)
1417
+ written = written[2..] if written.start_with?("::")
1418
+ return true if written == qualified
1419
+ return false unless qualified.end_with?(written)
1420
+
1421
+ qualified.getbyte(qualified.bytesize - written.bytesize - 1) == 58
1422
+ end
1423
+ private :names_class?
1424
+
1275
1425
  # Module-singleton call resolution (ADR-57 follow-up) — companion of {#user_def_for} for SINGLETON-side defs
1276
1426
  # (`def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the
1277
1427
  # `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The
@@ -1317,6 +1467,147 @@ module Rigor
1317
1467
  Inference::DefNodeResolver.resolve(entry)
1318
1468
  end
1319
1469
 
1470
+ # Issue #2515 — why the `<toplevel>` table's def for `method_name` may not be the method a receiverless call of that
1471
+ # name runs, or nil when the project's own tables show nothing else that could be. The table keeps one def per name
1472
+ # (the last writer's) and also files the defs of an anonymous `Class.new` body, so:
1473
+ #
1474
+ # - `:contested` — a def in a block or under a condition wrote the slot last;
1475
+ # - `:several` — the project holds more than one ownerless definition row of the name: two top-level defs (across
1476
+ # files, or redefined), a def in a block or in another def, `define_method` or `alias_method` of the name;
1477
+ # - `:main_singleton` — a `def self.name` (or a `class << self` def) at the top level defines it on `main`;
1478
+ # - `:aliased` — an `alias` statement or `alias_method` call names it as the new name;
1479
+ # - `:anonymous` — an anonymous class body defines it;
1480
+ # - `:object` — `Object` itself defines it (`class Object; def name`, `define_method`) or has a module prepended,
1481
+ # which Ruby searches ahead of the top-level def. A module included at the top level is searched after `Object`'s
1482
+ # own methods, so it is no reason.
1483
+ #
1484
+ # A name with no row at all (a scope built without the project pre-pass) is not `:several`: the rows say nothing.
1485
+ # The answer depends on every file, so an incremental run records the `defines:<name>` edge and the census edge of
1486
+ # an alias either way. The two project-wide scans are built once per table, not per call.
1487
+ def toplevel_def_ambiguity(method_name)
1488
+ name = method_name.to_sym
1489
+ record_toplevel_ambiguity_reads(name) if Analysis::DependencyRecorder.active?
1490
+ key = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
1491
+ return :contested if @discovery.contested_discovered_def_nodes.include?([key, name])
1492
+
1493
+ rows = toplevel_definition_rows[name]
1494
+ return :main_singleton if rows == MAIN_SINGLETON_ROWS
1495
+ return :several if rows > 1
1496
+ return :aliased if method_alias_names.include?(name)
1497
+
1498
+ return :anonymous if @discovery.discovered_def_nodes.any? do |class_name, defs|
1499
+ defs.key?(name) && class_name.start_with?(Type::AnonymousClassName::PREFIX)
1500
+ end
1501
+
1502
+ :object if object_answers_first?(name)
1503
+ end
1504
+
1505
+ # True when `Object` defines `name` itself or has a module prepended (see {#toplevel_def_ambiguity}).
1506
+ def object_answers_first?(name)
1507
+ !user_def_for("Object", name).nil? || discovered_method?("Object", name, :instance) ||
1508
+ !(@discovery.discovered_prepends["Object"] || EMPTY_PREPENDS).empty?
1509
+ end
1510
+ private :object_answers_first?
1511
+
1512
+ EMPTY_PREPENDS = [].freeze
1513
+ private_constant :EMPTY_PREPENDS
1514
+
1515
+ # True for a class or module the project declares, even one with no method of its own.
1516
+ def declared_class?(class_name) = @discovery.discovered_classes.key?(class_name.to_s)
1517
+
1518
+ def record_toplevel_ambiguity_reads(name)
1519
+ Analysis::DependencyRecorder.read_name(:defines, name)
1520
+ Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}")
1521
+ Analysis::DependencyRecorder.read_last_segment(:class, "Object")
1522
+ record_class_dependency("Object")
1523
+ end
1524
+ private :record_toplevel_ambiguity_reads
1525
+
1526
+ # Issue #2336 — whether the project gives `method_name` a body through an ownerless definition row
1527
+ # ({#toplevel_definition_rows}): a `define_method` / `define_singleton_method` / `alias_method` call with that
1528
+ # literal name on any receiver, a def in a block, or (with `top_level:`) a def outside every class body. The
1529
+ # callee mutation floor reads a call no def table answers as one such body may run. The answer depends on every
1530
+ # file, so an incremental run records the `defines:<name>` edge either way.
1531
+ def ownerless_definition?(method_name, top_level: true)
1532
+ Analysis::DependencyRecorder.read_name(:defines, method_name) if Analysis::DependencyRecorder.active?
1533
+ name = method_name.to_sym
1534
+ return true if top_level ? toplevel_definition_rows.key?(name) : deferred_definition_names.include?(name)
1535
+
1536
+ false
1537
+ end
1538
+
1539
+ # The names of the ownerless rows that claim no side ({#ownerless_definition?} without `top_level:`): a definer
1540
+ # call's literal name and a def in a block. Held by the table's identity, as {#toplevel_definition_rows} is.
1541
+ def deferred_definition_names
1542
+ ranges = @discovery.discovered_deferred_ranges
1543
+ slots = (Thread.current[DEFERRED_NAMES_KEY] ||= [])
1544
+ slot = slots.find { |entry| entry[0].equal?(ranges) }
1545
+ return slot[1] if slot
1546
+
1547
+ names = Set.new
1548
+ ranges.each_value do |entries|
1549
+ entries.each { |(_start, _finish, name, kind, owner)| names << name if name && kind.nil? && owner.nil? }
1550
+ end
1551
+ slots.unshift([ranges, names.freeze]).slice!(2..)
1552
+ names
1553
+ end
1554
+ private :deferred_definition_names
1555
+
1556
+ # Issue #2336 — {#module_function_def?}'s `:both` reading for a def in any file: whether `def_node`, an instance
1557
+ # def of `owner`, is a module function by a deferred-range row over it (`module_function def x`, or a bare
1558
+ # `module_function` before it), which the singleton def table does not mirror for the argument form.
1559
+ def module_function_row_def?(def_node, owner)
1560
+ start = def_node.location.start_offset
1561
+ name = def_node.name
1562
+ owner = owner.to_s
1563
+ discovered_deferred_ranges.each_value.any? do |entries|
1564
+ entries.any? do |(row_start, _finish, row_name, kind, row_owner)|
1565
+ row_start == start && kind == :both && row_name == name && row_owner == owner
1566
+ end
1567
+ end
1568
+ end
1569
+
1570
+ # `{ name => ownerless definition rows }` over `discovered_deferred_ranges`, {MAIN_SINGLETON_ROWS} for a name with
1571
+ # a singleton row. Held by the table's identity (a file's merged copy and the seed), released with the run.
1572
+ def toplevel_definition_rows
1573
+ ranges = @discovery.discovered_deferred_ranges
1574
+ slots = (Thread.current[TOPLEVEL_ROWS_KEY] ||= [])
1575
+ slot = slots.find { |entry| entry[0].equal?(ranges) }
1576
+ return slot[1] if slot
1577
+
1578
+ rows = Hash.new(0)
1579
+ ranges.each_value do |entries|
1580
+ entries.each do |(_start, _finish, name, kind, owner)|
1581
+ next if name.nil? || !owner.nil?
1582
+
1583
+ rows[name] = kind == :singleton || rows[name] == MAIN_SINGLETON_ROWS ? MAIN_SINGLETON_ROWS : rows[name] + 1
1584
+ end
1585
+ end
1586
+ slots.unshift([ranges, rows.freeze]).slice!(2..)
1587
+ rows
1588
+ end
1589
+ private :toplevel_definition_rows
1590
+
1591
+ # The names an `alias` statement or `alias_method` call introduces, from the global write census; held by the
1592
+ # census's identity as {#toplevel_definition_rows} is.
1593
+ def method_alias_names
1594
+ census = @discovery.discovered_global_write_census
1595
+ slots = (Thread.current[METHOD_ALIASES_KEY] ||= [])
1596
+ slot = slots.find { |entry| entry[0].equal?(census) }
1597
+ return slot[1] if slot
1598
+
1599
+ names = census.each_with_object(Set.new) { |entry, acc| acc << entry[1] if entry[0] == :method_alias }.freeze
1600
+ slots.unshift([census, names]).slice!(2..)
1601
+ names
1602
+ end
1603
+ private :method_alias_names
1604
+
1605
+ MAIN_SINGLETON_ROWS = -1
1606
+ TOPLEVEL_ROWS_KEY = RunScopedSlots.register(:__rigor_toplevel_definition_rows)
1607
+ METHOD_ALIASES_KEY = RunScopedSlots.register(:__rigor_method_alias_names)
1608
+ DEFERRED_NAMES_KEY = RunScopedSlots.register(:__rigor_deferred_definition_names)
1609
+ private_constant :MAIN_SINGLETON_ROWS, :TOPLEVEL_ROWS_KEY, :METHOD_ALIASES_KEY, :DEFERRED_NAMES_KEY
1610
+
1320
1611
  # Issue #316 — the CONFIDENCE-GATED companion of {#top_level_def_for}, and the only accessor the type
1321
1612
  # inference may bind through. {#top_level_def_for} stays unrestricted because it also serves the
1322
1613
  # *suppression* side (`call.unresolved-toplevel`, `call.undefined-method`): a name the project defines at
@@ -1970,7 +2261,7 @@ module Rigor
1970
2261
  EMPTY_PARAMETER_ENVELOPES = {}.freeze
1971
2262
  private_constant :EMPTY_PARAMETER_ENVELOPES
1972
2263
 
1973
- # v0.1.2 — per-class table mapping `method_name (Symbol) → :public | :private | :protected`. Populated by
2264
+ # v0.1.2 — per-class table mapping `method_name → :public | :private | :protected`. Populated by
1974
2265
  # `ScopeIndexer` for every `def` and `attr_*` member it sees inside a class body, with the visibility taken from
1975
2266
  # a wrapping `private def` / `private attr_reader` call, else the surrounding `private` / `protected` / `public`
1976
2267
  # modifier state, plus any post-hoc `private :name, ...` named-argument calls. Consumed by the
@@ -1989,13 +2280,14 @@ module Rigor
1989
2280
  table[singleton ? [:singleton, method_name.to_sym] : method_name.to_sym]
1990
2281
  end
1991
2282
 
1992
- # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap (ROADMAP § Type-language / engine —
1993
- # indexed-collection narrowing through `Hash[k] ||= default`). After `receiver[key] ||= default`, the next
2283
+ # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap. After `receiver[key] ||= default`, the next
1994
2284
  # read at `receiver[key]` is known non-nil; recording the post-`||=` type keyed on
1995
2285
  # `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper's `[]` dispatch hand back the
1996
2286
  # narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in
1997
2287
  # `Inference::StatementEvaluator`.
1998
2288
  def indexed_narrowing(receiver_kind, receiver_name, key)
2289
+ return nil if @indexed_narrowings.empty?
2290
+
1999
2291
  @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)]
2000
2292
  end
2001
2293
 
@@ -2007,6 +2299,8 @@ module Rigor
2007
2299
  end
2008
2300
 
2009
2301
  def without_indexed_narrowing(receiver_kind, receiver_name, key)
2302
+ return self if @indexed_narrowings.empty?
2303
+
2010
2304
  lookup = indexed_key(receiver_kind, receiver_name, key)
2011
2305
  return self unless @indexed_narrowings.key?(lookup)
2012
2306
 
@@ -2021,14 +2315,15 @@ module Rigor
2021
2315
  rebuild(indexed_narrowings: new_table)
2022
2316
  end
2023
2317
 
2024
- # Closes the "stable receiver method-chain narrowing" gap (ROADMAP § Future cycles / Type-language / engine —
2025
- # "Method-call receiver narrowing across stable receivers"; 2026-05-28 Redmine survey). After
2318
+ # Closes the "stable receiver method-chain narrowing" gap (2026-05-28 Redmine survey). After
2026
2319
  # `if x.last.is_a?(Array)` the dominated body's `x.last` reads MUST observe the truthy-narrowed type; the same
2027
2320
  # chain reaching the falsey edge observes the negative narrowing.
2028
2321
  #
2029
2322
  # Address shape mirrors {.indexed_narrowing}: stable root variable + no-arg single-hop method name. See
2030
2323
  # {ChainKey} for the precise contract.
2031
2324
  def method_chain_narrowing(receiver_kind, receiver_name, method_name)
2325
+ return nil if @method_chain_narrowings.empty?
2326
+
2032
2327
  @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)]
2033
2328
  end
2034
2329
 
@@ -2040,6 +2335,8 @@ module Rigor
2040
2335
  end
2041
2336
 
2042
2337
  def without_method_chain_narrowing(receiver_kind, receiver_name, method_name)
2338
+ return self if @method_chain_narrowings.empty?
2339
+
2043
2340
  lookup = chain_key(receiver_kind, receiver_name, method_name)
2044
2341
  return self unless @method_chain_narrowings.key?(lookup)
2045
2342
 
@@ -2149,11 +2446,13 @@ module Rigor
2149
2446
 
2150
2447
  private
2151
2448
 
2152
- # The marks {#==} compares: ADR-58's, issue #667's and the repeated `||=` sites, and #1429's guard state.
2449
+ # The marks {#==} compares: ADR-58's, issue #667's, the repeated `||=` sites, #2555's construction facts (they
2450
+ # change what a mutation seam answers), and #1429's guard state.
2153
2451
  def same_marks?(other)
2154
2452
  @declaration_sourced == other.declaration_sourced &&
2155
2453
  @published_constant_sourced == other.published_constant_sourced &&
2156
2454
  @repeated_or_writes == other.repeated_or_writes &&
2455
+ @construction_facts == other.construction_facts &&
2157
2456
  same_guard_state?(other)
2158
2457
  end
2159
2458
 
@@ -2162,7 +2461,15 @@ module Rigor
2162
2461
  @bot_guard_classes == other.bot_guard_classes
2163
2462
  end
2164
2463
 
2464
+ # `construction_facts` is the one positional parameter. CRuby records which keywords a call left out in a bit
2465
+ # field indexed by each keyword's position among ALL the method's keywords, required and optional
2466
+ # (`VM_KW_SPECIFIED_BITS_MAX`, 31); a keyword past that index whose default is not a literal makes every call that
2467
+ # omits it allocate the record instead. A 32nd keyword here did that on every scope transition, about 1% of an
2468
+ # analysis's allocations (issue #2555). `#initialize` is already past the index: `bot_guard_classes` and
2469
+ # `declared_refinements` take the allocating path whenever a caller omits them. The next Scope field crosses it in
2470
+ # this method again, so it needs a positional slot or a restructuring, not a 32nd keyword.
2165
2471
  def rebuild(
2472
+ construction_facts = @construction_facts,
2166
2473
  locals: @locals, fact_store: @fact_store, self_type: @self_type,
2167
2474
  ivars: @ivars, cvars: @cvars, globals: @globals,
2168
2475
  discovery: @discovery,
@@ -2213,7 +2520,7 @@ module Rigor
2213
2520
  plugin_typed_calls: plugin_typed_calls,
2214
2521
  optimistic_origins: optimistic_origins,
2215
2522
  optimistic_locals: optimistic_locals,
2216
- optimistic_ivars: optimistic_ivars,
2523
+ optimistic_ivars: optimistic_ivars, construction_facts: construction_facts,
2217
2524
  repeated_or_writes: repeated_or_writes,
2218
2525
  match_frame: match_frame,
2219
2526
  constant_narrowings: constant_narrowings,
@@ -2223,11 +2530,18 @@ module Rigor
2223
2530
  )
2224
2531
  end
2225
2532
 
2533
+ # A join with an empty side has no key in common (usually both are empty, the common case at a branch merge), so
2534
+ # its answer is this one frozen hash rather than a fresh one.
2535
+ EMPTY_BINDINGS = {}.freeze
2536
+ private_constant :EMPTY_BINDINGS
2537
+
2226
2538
  def join_bindings(left, right)
2227
2539
  # Keys present in both, unioned. Iterating `left` and probing `right.key?` yields the same keys in the same
2228
2540
  # order as the prior `(left.keys & right.keys)` while avoiding the two key arrays and the intersection
2229
2541
  # array — this is the control-flow join, run at every branch merge, and was a top allocation site (~75% of
2230
2542
  # `Hash#keys`).
2543
+ return EMPTY_BINDINGS if left.empty? || right.empty?
2544
+
2231
2545
  result = {}
2232
2546
  left.each do |name, ltype|
2233
2547
  next unless right.key?(name)
@@ -2306,6 +2620,8 @@ module Rigor
2306
2620
  optimistic_origins: @optimistic_origins,
2307
2621
  optimistic_locals: join_optimistic_marks(@optimistic_locals, other.optimistic_locals),
2308
2622
  optimistic_ivars: join_optimistic_marks(@optimistic_ivars, other.optimistic_ivars),
2623
+ # Issue #2555 L3 — INTERSECTION: a construction fact holds after a merge only when both arms established it.
2624
+ construction_facts: join_construction_facts(@construction_facts, other.construction_facts),
2309
2625
  # UNION, the published-constant mark's direction: the mark only withholds the memoizing `||=`
2310
2626
  # reading, so keeping a site either arm holds is the wider answer.
2311
2627
  repeated_or_writes: join_repeated_or_writes(other),
@@ -2403,6 +2719,31 @@ module Rigor
2403
2719
  .freeze
2404
2720
  end
2405
2721
 
2722
+ # Issue #2555 L3 — a fact survives a merge only for a name both arms hold it for, and only the facts both agree
2723
+ # on. Zero-alloc when either side is empty or both are the same table.
2724
+ def join_construction_facts(mine, theirs)
2725
+ return mine if mine.equal?(theirs)
2726
+ return EMPTY_ORIGINS if mine.empty? || theirs.empty?
2727
+
2728
+ joined = {}
2729
+ mine.each do |name, facts|
2730
+ their_facts = theirs[name]
2731
+ next if their_facts.nil?
2732
+
2733
+ common = facts == their_facts ? facts : join_fact_sets(facts, their_facts)
2734
+ joined[name] = common unless common.empty?
2735
+ end
2736
+ joined.empty? ? EMPTY_ORIGINS : joined.freeze
2737
+ end
2738
+
2739
+ # `:grown` joins by UNION among the names both arms keep: a store on either arm may reach the carrier, and the
2740
+ # fact only ever floors it ({Inference::MutationRejoin.floors_grown_carrier?}). Every other fact intersects.
2741
+ def join_fact_sets(facts, their_facts)
2742
+ common = facts & their_facts
2743
+ common << :grown if facts.include?(:grown) || their_facts.include?(:grown)
2744
+ common.freeze
2745
+ end
2746
+
2406
2747
  # ADR-82 WD1 — rebinding drops any propagated origin for the name (the new value's provenance is set
2407
2748
  # afterward by `with_local_origin` when it is a `Dynamic` with a recorded cause). Zero-alloc when the name
2408
2749
  # carries no origin — the overwhelming common case, so this stays off the with_local hot-path budget.
@@ -2464,19 +2805,12 @@ module Rigor
2464
2805
  end
2465
2806
 
2466
2807
  def indexed_key(receiver_kind, receiver_name, key)
2467
- IndexedKey.new(
2468
- receiver_kind: receiver_kind.to_sym,
2469
- receiver_name: receiver_name.to_sym,
2470
- key: key
2471
- )
2808
+ # Positional `new` (field-definition order) skips the keyword hash a keyword `new` allocates.
2809
+ IndexedKey.new(receiver_kind.to_sym, receiver_name.to_sym, key)
2472
2810
  end
2473
2811
 
2474
2812
  def chain_key(receiver_kind, receiver_name, method_name)
2475
- ChainKey.new(
2476
- receiver_kind: receiver_kind.to_sym,
2477
- receiver_name: receiver_name.to_sym,
2478
- method_name: method_name.to_sym
2479
- )
2813
+ ChainKey.new(receiver_kind.to_sym, receiver_name.to_sym, method_name.to_sym)
2480
2814
  end
2481
2815
 
2482
2816
  def drop_indexed_narrowings_for(receiver_kind, receiver_name)
@@ -55,6 +55,10 @@ module Rigor
55
55
  # generic T`) and `sig/` does not declare it yet. sig-gen writes no class type parameters, and a header
56
56
  # without them fails the class's definition build.
57
57
  inline_generic_class: "sig.skipped.inline-generic-class",
58
+ # #2436 — the member's class, or one it is nested in, is declared generic by loaded RBS with a parameter
59
+ # that has a bound or a default, and `sig/` does not declare it. A header copied from that declaration
60
+ # could resolve the bound differently, and one without parameters fails the class's definition build.
61
+ generic_reopening: "sig.skipped.generic-reopening",
58
62
  # ADR-112 WD4 — the member is declared inline and in `sig/`, and the two disagree. A refusal, not a
59
63
  # skip: `--write` and `--check` exit 1 until a person reconciles them or passes `--overwrite`.
60
64
  inline_differs: "sig.skipped.inline-differs",
@@ -82,8 +82,8 @@ module Rigor
82
82
  #
83
83
  # @param declared — whether the method already carries an authored bound of its own
84
84
  # ({Effects::EnvelopeIndex}); see gate 0.
85
- # @return `[Array<String> annotations, Symbol|nil reason]`. An empty
86
- # array with a `nil` reason is "nothing to say about this method".
85
+ # @return the annotation lines to render, then the reason to report. An empty list with a `nil` reason
86
+ # is "nothing to say about this method".
87
87
  def decide(entry, envelopes: false, declared: false)
88
88
  return [[], :withheld_declared] if declared
89
89
  return [[], nil] if entry.nil?
@@ -133,7 +133,7 @@ module Rigor
133
133
 
134
134
  # @param path — the `.rb` file the def came from, for a `match:`-selected
135
135
  # `effects.envelopes:` entry.
136
- # @return `[Array<String>, Symbol|nil]` — see {EffectAnnotation.decide}.
136
+ # @return the annotation lines and the reason, as {EffectAnnotation.decide} gives them.
137
137
  def annotate(class_name:, method_name:, kind:, path: nil)
138
138
  key = EffectAnnotation.key_for(class_name, method_name, kind)
139
139
  return [[], nil] if key.nil?