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
@@ -15,6 +15,7 @@ require_relative "../cache/file_digest"
15
15
  require_relative "../analysis/check_rules/published_constant_guard"
16
16
  require_relative "anonymous_meta_class"
17
17
  require_relative "def_handle"
18
+ require_relative "definers_index"
18
19
  require_relative "fresh_frame_blocks"
19
20
  require_relative "global_write_census"
20
21
  require_relative "guard_rebinding"
@@ -45,6 +46,9 @@ require_relative "scope_indexer/certainty"
45
46
  require_relative "scope_indexer/member_layouts_collector"
46
47
  require_relative "scope_indexer/declared_ivar_types"
47
48
  require_relative "scope_indexer/declared_param_writes"
49
+ require_relative "scope_indexer/constant_ivar_value"
50
+ require_relative "scope_indexer/ivar_contributions"
51
+ require_relative "scope_indexer/empty_seed"
48
52
 
49
53
  module Rigor
50
54
  module Inference
@@ -67,8 +71,8 @@ module Rigor
67
71
  # ```
68
72
  #
69
73
  # Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the
70
- # fact) yield the `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only;
71
- # the indexer itself never exposes a way to update it past construction.
74
+ # fact) yield the seeded `default_scope`. The returned Hash is mutable in principle but callers MUST treat it as
75
+ # read-only; the indexer itself never exposes a way to update it past construction.
72
76
  # rubocop:disable-next Metrics/ModuleLength
73
77
  module ScopeIndexer
74
78
  # Issue #644 — the census descriptor for a constant write that publishes no value: an unfoldable or
@@ -109,13 +113,15 @@ module Rigor
109
113
  # `Dynamic[top]`. Off for every other reader, so parameter inference
110
114
  # and sig-gen observe the clause's plain entry.
111
115
  # @return identity-comparing
112
- # table whose default value is `default_scope`.
116
+ # table whose default value is the seeded `default_scope`.
113
117
  # The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
114
118
  # nothing-to-re-anchor walk allocates none.
115
119
  EMPTY_RENAMES = {}.freeze
116
120
 
117
121
  # No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
118
122
  EMPTY_PATH_SET = Set.new.freeze
123
+ # The {IvarWriterCensus} entry kinds the project pre-pass tags with the file that records them.
124
+ PATH_TAGGED_KINDS = %i[own_write own_write_type].to_set.freeze
119
125
 
120
126
  # `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
121
127
  # `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
@@ -233,9 +239,9 @@ module Rigor
233
239
  end
234
240
 
235
241
  # Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
236
- # onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
237
- # file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
238
- # {#merge_project_method_indexes} without walking the file a second time.
242
+ # onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns
243
+ # `[scope, [file_def_nodes, file_envelopes, file_siblings]]` so the caller can thread the def-node and issue #992
244
+ # envelope tables into {#merge_project_method_indexes} without walking the file a second time.
239
245
  #
240
246
  # ADR-119 WD3 — the file's `possible_discovered_methods` folds over the seed's by {#fold_possible_methods}, so a
241
247
  # name the file defines certainly is no longer possible and a name only the file's possible contribution
@@ -306,8 +312,8 @@ module Rigor
306
312
  siblings = per_file_siblings(seeded_scope.discovery.sibling_tables, file_tables[0], file_tables[2],
307
313
  file_singleton, file_visibilities)
308
314
  extends, methods_table, unpositioned =
309
- merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
310
- mixin_unpositioned)
315
+ merge_and_fold_extends(default_scope, root, [def_nodes, file_tables[0]],
316
+ [singleton_def_nodes, file_singleton[0]], seeded_scope, siblings, mixin_unpositioned)
311
317
 
312
318
  seeded_scope.with_discovery(
313
319
  seeded_scope.discovery.with(
@@ -421,13 +427,23 @@ module Rigor
421
427
  # ADR-119 WD1 — the unpositioned union (the seed's, the mixin walks' and this file's extend side) is settled
422
428
  # first, because the fold reads it to tell a certain extend edge from a listed one. `siblings` is written by
423
429
  # the fold.
424
- def merge_and_fold_extends(default_scope, root, def_nodes, singleton_def_nodes, seeded_scope, siblings,
430
+ # `def_pair` and `singleton_pair` are `[merged table, this file's own]`; the merged tables are frozen once the
431
+ # fold, their last in-place writer, is done ({#freeze_def_tables}).
432
+ def merge_and_fold_extends(default_scope, root, def_pair, singleton_pair, seeded_scope, siblings,
425
433
  mixin_unpositioned)
434
+ def_nodes = def_pair.first
435
+ singleton_def_nodes = singleton_pair.first
426
436
  file_extends, extends, extend_unpositioned = merge_extend_tables(default_scope, root)
427
437
  unpositioned = union_unpositioned(default_scope.discovery.unpositioned_mixins, *mixin_unpositioned,
428
438
  extend_unpositioned)
429
439
  methods_table = fold_per_file_extends(file_extends, def_nodes, singleton_def_nodes, seeded_scope, siblings,
430
440
  unpositioned)
441
+ # The fold also writes the singleton hash of each class this file `extend`s, declared there or not.
442
+ file_extends.each_key { |owner| singleton_def_nodes[owner]&.freeze }
443
+ freeze_def_tables(def_pair, singleton_pair)
444
+ # #2300 — the override gate's definers index then inverts the seed once per run, not once per file.
445
+ DefinersIndex.record_layers(def_nodes, default_scope.discovered_def_nodes)
446
+ DefinersIndex.record_layers(singleton_def_nodes, default_scope.discovered_singleton_def_nodes)
431
447
  [extends, methods_table, unpositioned]
432
448
  end
433
449
 
@@ -458,6 +474,20 @@ module Rigor
458
474
  walked.fetch(:header_nestings)).freeze]
459
475
  end
460
476
 
477
+ # Issue #2330 — the per-file def table is read by identity: the callee-chain census memo is keyed by the
478
+ # {Scope::DiscoveryIndex} that carries it, which is only sound while the table is never written in place.
479
+ # Frozen after the extends fold, the last in-place writer; the seed's per-class hashes are frozen already.
480
+ #
481
+ # Only the per-class hashes of the classes this file declares are frozen here (the merge made or took
482
+ # them); a seed-only class's hash was frozen by `finalize_def_index`, so walking the whole table per file is
483
+ # needless.
484
+ def freeze_def_tables(*pairs)
485
+ pairs.each do |table, file_table|
486
+ file_table.each_key { |owner| table[owner].freeze }
487
+ table.freeze
488
+ end
489
+ end
490
+
461
491
  # The instance-side def-node table and its issue #681 nesting twin, each merged over the cross-file seed.
462
492
  # Returned as a pair so the two stay written together: a node the merge keeps must be the same object the
463
493
  # nesting table keys, and they are only that if both halves take the same file's walk.
@@ -529,20 +559,28 @@ module Rigor
529
559
  mutated_ivars = {}
530
560
  read_before_write = {}
531
561
  init_writes = {}
532
- # WD3 — per-class summary of `{class_name => {method_name => Set<ivar names definitely assigned non-nil on every
533
- # completing path>}}`, consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through
534
- # an unconditional same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised
535
- # by class.
562
+ # WD3 — per class, then per method, the ivars the method definitely assigns non-nil on every completing path,
563
+ # consulted by `dead_transient_nil_writes` so a ctor that reassigns `@x` indirectly through an unconditional
564
+ # same-class method call (`mask!`) credits the overwrite. Built once per program here, memoised by class.
536
565
  method_assign_effects = build_method_assign_effects(root)
566
+ # #2291 — an `@x = LIT` write reads the file's constant table, built on the first such write.
567
+ accumulator[CONSTANT_IVAR_VALUES] = ConstantIvarValue.new(root, default_scope)
537
568
  walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
538
569
  read_before_write, init_writes, method_assign_effects)
570
+ accumulator.delete(CONSTANT_IVAR_VALUES)
539
571
  reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
540
572
  exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
541
573
  merge_held_ivar_writes!(accumulator)
542
574
  record_aliased_ivar_mutations!(root, mutated_ivars)
543
575
  widen_mutated_ivar_entries!(accumulator, mutated_ivars)
576
+ # #2170 — an ancestor's writes as this file's census types them, before its read-before-write `nil`.
577
+ writes = accumulator.transform_values(&:dup)
544
578
  contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
545
579
  make_reflective_classes_gradual!(accumulator, reflective_opaque)
580
+ # #2079 / #2170 — another file's writes, and the chain's modules' and superclasses', join existing entries.
581
+ IvarContributions.join!(accumulator, writes, default_scope)
582
+ # An in-place mutation the file observes widens a joined value too (a same-file ancestor's String literal).
583
+ widen_mutated_ivar_entries!(accumulator, mutated_ivars)
546
584
  # #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
547
585
  DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
548
586
  accumulator.transform_values(&:freeze).freeze
@@ -628,8 +666,8 @@ module Rigor
628
666
  end
629
667
  end
630
668
 
631
- # `{class_name => {method_name => Set<ivar name>}}` for methods that hand an ivar back, dropping the empty sets
632
- # so the caller's `next if per_class.nil?` skips a whole class in one lookup.
669
+ # Per class, then per method, the ivars each method hands back, dropping the empty sets so the caller's
670
+ # `next if per_class.nil?` skips a whole class in one lookup.
633
671
  def returned_ivars_by_method(defs)
634
672
  defs.each_with_object({}) do |(class_name, methods), acc|
635
673
  methods.each do |method_name, def_node|
@@ -718,9 +756,8 @@ module Rigor
718
756
  # The widening uses `MutationWidening.widen_for_mutator` — the same primitive
719
757
  # `Inference::StatementEvaluator#eval_call` applies for per-method-body widening on a local / ivar receiver. The
720
758
  # class-level pass extends that primitive's reach so a `Tuple`-seeded ivar in `initialize` is observed as
721
- # `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap noted in
722
- # ROADMAP § Future cycles / Type-language / engine ("Tuple / HashShape widening for ivar-seeded literals after
723
- # mutation"; Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
759
+ # `Nominal[Array]` at the entry of every OTHER method body in the class — closing the cross-method gap
760
+ # (Redmine 6.1.2 `Redmine::Views::Builders::Structure` is the canonical worked site).
724
761
  #
725
762
  # Always-safe: the widening can only LOSE precision; the underlying nominal (`Array` / `Hash`) and the element
726
763
  # union are preserved.
@@ -765,18 +802,27 @@ module Rigor
765
802
  end
766
803
 
767
804
  widen_member_for_lookup_mutators(member, observed_methods)
768
- when Type::Constant
769
- # `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every
770
- # other method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`.
771
- return member unless StringMutation.constant?(member) &&
772
- observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
773
-
774
- Type::Combinator.nominal_of("String")
805
+ when Type::Constant then widen_constant_for_observed_mutators(member, observed_methods)
775
806
  else
776
807
  member
777
808
  end
778
809
  end
779
810
 
811
+ # `@s = +"ab"` in one method and `@s << "c"` in another: without this arm the seed stayed pinned at every other
812
+ # method's entry, and `@s == "ab"` folded always-truthy on a receiver that holds `"abc"`. A constant-folded Set
813
+ # (`@s = Set["a"]`, then `@s.clear; @s << 1` elsewhere — issue #2294) takes the untyped floor the Tuple arm takes.
814
+ def widen_constant_for_observed_mutators(member, observed_methods)
815
+ if SetMutation.constant?(member)
816
+ return member unless observed_methods.any? { |m| SetMutation::MUTATORS.include?(m) }
817
+
818
+ return Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
819
+ end
820
+ return member unless StringMutation.constant?(member) &&
821
+ observed_methods.any? { |m| StringMutation::MUTATORS.include?(m) }
822
+
823
+ Type::Combinator.nominal_of("String")
824
+ end
825
+
780
826
  # A `HashShape` ivar seed some method gave a default, a default proc or identity keys, and no method stored
781
827
  # into, removed from or rewrote: its pairs are the seed's own, so it takes the per-method {HashLookupMutation}
782
828
  # widening rather than the untyped floor above — a present key keeps its value across methods, a missing one
@@ -1316,9 +1362,9 @@ module Rigor
1316
1362
  # not `InstanceVariableWriteNode`s, so the generic descent below never records them as writes. Left unhandled,
1317
1363
  # an ivar written only via massign in `initialize` stays absent from `init_writes`, and
1318
1364
  # `contribute_read_before_write_nil!` then unions a spurious `nil` into its class-ivar seed — masking a
1319
- # genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any target is
1320
- # committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every ivar
1321
- # target as written.
1365
+ # genuinely-typed `@m` (e.g. `Tuple[]`) as `T | nil` at every sibling read. The RHS runs before any
1366
+ # target is committed, so descend into `value` FIRST (an ivar read there is read-before-write), then mark every
1367
+ # ivar target as written.
1322
1368
  if node.is_a?(Prism::MultiWriteNode)
1323
1369
  detect_read_before_write(node.value, seen_writes, read_first) if node.value
1324
1370
  detect_multi_write_target_writes(node, seen_writes, read_first)
@@ -1445,13 +1491,21 @@ module Rigor
1445
1491
  method_name, receiver = mutation_target(node)
1446
1492
  return if method_name.nil?
1447
1493
  return unless receiver.is_a?(Prism::InstanceVariableReadNode)
1448
- return unless MutationWidening::SHAPE_MUTATORS.include?(method_name)
1494
+ return unless ivar_census_mutator?(method_name)
1449
1495
 
1450
1496
  per_class = (mutated_ivars[class_name] ||= {})
1451
1497
  per_ivar = (per_class[receiver.name] ||= Set.new)
1452
1498
  per_ivar << method_name
1453
1499
  end
1454
1500
 
1501
+ # The Set-only names (`add`, `merge`, … — issue #2294) are recorded here rather than in `SHAPE_MUTATORS`, which
1502
+ # every scan reads by name alone: there `merge` would make `Hash#merge`, which returns a new Hash, a mutation of
1503
+ # its receiver. The census is safe to over-collect, since {#widen_member_for_observed_mutators} reads each name
1504
+ # against the table of the carrier it widens.
1505
+ def ivar_census_mutator?(method_name)
1506
+ MutationWidening::SHAPE_MUTATORS.include?(method_name) || SetMutation::MUTATORS.include?(method_name)
1507
+ end
1508
+
1455
1509
  # `[mutator name, receiver node]` for a node that mutates its receiver, `nil` otherwise.
1456
1510
  #
1457
1511
  # `@h[k] ||= v` and its `&&=` / `+=` siblings store through `[]=` but are not `[]=` CallNodes; missing them left
@@ -1586,10 +1640,9 @@ module Rigor
1586
1640
  SAME_CLASS_CALL_DEPTH_CAP = 3
1587
1641
  private_constant :SAME_CLASS_CALL_DEPTH_CAP
1588
1642
 
1589
- # WD3 — builds the per-class definite-assignment summary `{class_name => {method_name => Set<ivar names assigned
1590
- # non-nil on every completing path>}}`. Used so a ctor's `dead_transient_nil_writes` can credit an indirect
1591
- # overwrite through an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via
1592
- # `mask!`).
1643
+ # WD3 — builds the per-class definite-assignment summary: per class, then per method, the ivars assigned non-nil
1644
+ # on every completing path. Used so a ctor's `dead_transient_nil_writes` can credit an indirect overwrite through
1645
+ # an unconditionally-called same-class method (ipaddr's `initialize` reassigns `@mask_addr` via `mask!`).
1593
1646
  #
1594
1647
  # Each method's set is computed by the same suffix definite-assignment analysis used for the ctor seed, run from
1595
1648
  # the method body's first statement for every ivar the method writes anywhere. Same-class calls inside a method
@@ -1976,10 +2029,11 @@ module Rigor
1976
2029
  end
1977
2030
 
1978
2031
  def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
1979
- rvalue_type = declared || scope.type_of(node.value)
2032
+ rvalue_type = declared || accumulator[CONSTANT_IVAR_VALUES]&.type_of(node.value, scope) ||
2033
+ scope.type_of(node.value)
1980
2034
 
1981
2035
  # `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
1982
- # (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
2036
+ # (defensive ivar-init with a nil / false rvalue). When the rvalue is itself a falsey
1983
2037
  # Constant, `union(rvalue, Constant[nil])` collapses (for `nil`) or doesn't widen the type's truthiness profile
1984
2038
  # (for `false`) — the predicate `unless @x` then folds to a single `Constant[nil]` / `Constant[false]` and the
1985
2039
  # `flow.always-truthy-condition` / `-always-falsey-` rule false-fires on the no-op-but-documented-default idiom.
@@ -1989,6 +2043,8 @@ module Rigor
1989
2043
  # `lib/tdiary/configuration.rb:157` for the worked site.
1990
2044
  return if guarded && falsey_constant?(rvalue_type)
1991
2045
 
2046
+ rvalue_type = EmptySeed.erase(node.value, rvalue_type)
2047
+
1992
2048
  rvalue_type = Type::Combinator.union(rvalue_type, Type::Combinator.constant_of(nil)) if guarded
1993
2049
  accumulate_ivar_type(accumulator, class_name, node.name, rvalue_type)
1994
2050
  end
@@ -2018,7 +2074,7 @@ module Rigor
2018
2074
  # `nil`, the guard bound its own ivar, and `ExpressionTyper#compound_write_value` read
2019
2075
  # `truthy(nil) | bot` — an instance method that sig-gen declared `-> bot`.
2020
2076
  def record_ivar_or_write(node, scope, class_name, accumulator)
2021
- rvalue_type = scope.type_of(node.value)
2077
+ rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2022
2078
  return if Narrowing.narrow_truthy(rvalue_type).is_a?(Type::Bot)
2023
2079
 
2024
2080
  accumulate_ivar_type(accumulator, class_name, node.name,
@@ -2182,7 +2238,9 @@ module Rigor
2182
2238
  CLASS_BODY_WRITES = :class_body_writes
2183
2239
  # The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
2184
2240
  REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
2185
- private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES
2241
+ # The census's {ConstantIvarValue}, which {#record_ivar_write} reads.
2242
+ CONSTANT_IVAR_VALUES = :constant_ivar_values
2243
+ private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES, :CONSTANT_IVAR_VALUES
2186
2244
 
2187
2245
  # Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
2188
2246
  # multi-write (parallel-assignment) collectors.
@@ -2215,7 +2273,11 @@ module Rigor
2215
2273
 
2216
2274
  rhs_type = scope.type_of(node.value)
2217
2275
  ivars = MultiTargetBinder.bind_marked(node, rhs_type, scope: scope, soften_slots: false).ivars
2218
- ivars.each { |name, type| accumulate_ivar_type(accumulator, class_name, name, type) }
2276
+ # #2499 — `@a, @b = {}, []` erases each empty seed as a single write does.
2277
+ values = EmptySeed.multi_write_values(node)
2278
+ ivars.each do |name, type|
2279
+ accumulate_ivar_type(accumulator, class_name, name, EmptySeed.erase(values[name], type))
2280
+ end
2219
2281
  end
2220
2282
 
2221
2283
  def falsey_constant?(type)
@@ -2382,7 +2444,7 @@ module Rigor
2382
2444
  end
2383
2445
 
2384
2446
  def record_cvar_write(node, scope, class_name, accumulator)
2385
- rvalue_type = scope.type_of(node.value)
2447
+ rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2386
2448
  accumulator[class_name] ||= {}
2387
2449
  existing = accumulator[class_name][node.name]
2388
2450
  accumulator[class_name][node.name] =
@@ -2504,7 +2566,7 @@ module Rigor
2504
2566
  end
2505
2567
 
2506
2568
  def record_global_write(node, scope, accumulator)
2507
- rvalue_type = scope.type_of(node.value)
2569
+ rvalue_type = EmptySeed.erase(node.value, scope.type_of(node.value))
2508
2570
  existing = accumulator[node.name]
2509
2571
  accumulator[node.name] =
2510
2572
  existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
@@ -3269,7 +3331,7 @@ module Rigor
3269
3331
  unless owner.empty?
3270
3332
  body_scope = census_body_scope(default_scope, enclosing_prefix, Type::Combinator.singleton_of(owner))
3271
3333
  end
3272
- rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
3334
+ rvalue_type = meta_new_constant_type(node, full) || EmptySeed.erase(node.value, body_scope.type_of(node.value))
3273
3335
  existing = accumulator[full]
3274
3336
  accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
3275
3337
  end
@@ -3567,7 +3629,8 @@ module Rigor
3567
3629
  REFINEMENT_WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
3568
3630
  REFINE_WORD = /\brefine\b/
3569
3631
  HEREDOC_OPENER = /<<[~-]?["'`A-Za-z_]/
3570
- private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER
3632
+ COMPARISON_OPERATORS = Set[:==, :!=, :===].freeze
3633
+ private_constant :EMPTY_REFINEMENTS, :REFINEMENT_WILDCARD, :REFINE_WORD, :HEREDOC_OPENER, :COMPARISON_OPERATORS
3571
3634
 
3572
3635
  NO_OFFSETS = [].freeze
3573
3636
  private_constant :NO_OFFSETS
@@ -3660,7 +3723,8 @@ module Rigor
3660
3723
  # `self`, charged to its module or the wildcard), `:literal` (A3's `:refine` / eval-string literal), `:nested` (a
3661
3724
  # `refine` in a refine body, or in a singleton method of one), `:dsl` (where `self` is a class, #1689) and
3662
3725
  # `:raises` (no block, a Proc as the block, another receiver, or a computed target where `self` is `main`), and
3663
- # `:data` (a `:refine` literal where `self` is a plain class or one of its instances).
3726
+ # `:data` (a `:refine` literal where `self` is a plain class or one of its instances), and `:inert` (issue #2203:
3727
+ # a `:refine` Symbol that is a hash key or a comparison operand, which no call reads as a method name).
3664
3728
  def refine_census(root)
3665
3729
  census = []
3666
3730
  refinements = build_methods_and_def_nodes(root, census: census)[3]
@@ -3839,6 +3903,8 @@ module Rigor
3839
3903
  end
3840
3904
 
3841
3905
  census_call(node, qualified_prefix, tables, refine_body)
3906
+ when Prism::AssocNode
3907
+ census_assoc_key(node, tables)
3842
3908
  when Prism::SymbolNode
3843
3909
  census_symbol(node, tables)
3844
3910
  end
@@ -3894,6 +3960,8 @@ module Rigor
3894
3960
  arguments = node.arguments&.arguments
3895
3961
  return if arguments.nil?
3896
3962
 
3963
+ return census_compared_literal(arguments.first, tables) if COMPARISON_OPERATORS.include?(node.name)
3964
+
3897
3965
  strings = RefineCensus.string_literal_call?(node)
3898
3966
  arguments.each do |argument|
3899
3967
  next unless RefineCensus.refine_symbol?(argument) || (strings && RefineCensus.refine_string?(argument))
@@ -3928,6 +3996,26 @@ module Rigor
3928
3996
 
3929
3997
  def constant_shaped?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
3930
3998
 
3999
+ # Issue #2203 — a `:refine` Symbol a comparison reads (`node.name == :refine`, `!=`, `===`) is a value compared,
4000
+ # never a method name a call runs: it is inert (`:inert`), and records no row.
4001
+ def census_compared_literal(argument, tables)
4002
+ return unless RefineCensus.refine_symbol?(argument) && !tables.literal_seen?(argument)
4003
+
4004
+ tables.see_literal(argument)
4005
+ tables.account(argument, :inert)
4006
+ end
4007
+
4008
+ # Issue #2203 — a `:refine` Symbol that is a hash key (`h(x, refine: true)`, `{ refine: 1 }`, `{ :refine => 1 }`)
4009
+ # names an option, not `Module#refine`: it is inert (`:inert`), and records no row. A `:refine` value stays a
4010
+ # literal.
4011
+ def census_assoc_key(node, tables)
4012
+ key = node.key
4013
+ return unless RefineCensus.refine_symbol?(key) && !tables.literal_seen?(key)
4014
+
4015
+ tables.see_literal(key)
4016
+ tables.account(key, :inert)
4017
+ end
4018
+
3931
4019
  # A3 — a `:refine` Symbol anywhere else (`alias my_refine refine`, `%i[refine]`, a hash) names `Module#refine`
3932
4020
  # for code the walk cannot follow, run on the module the in-effect walk says `self` is there; where `self` is a
3933
4021
  # plain class or one of its instances it is data.
@@ -4199,6 +4287,8 @@ module Rigor
4199
4287
  end
4200
4288
  end
4201
4289
  return
4290
+ when Prism::AssocNode
4291
+ census_assoc_key(node, methods_acc) if methods_acc.mentions_refine?
4202
4292
  when Prism::SymbolNode
4203
4293
  census_symbol(node, methods_acc) if methods_acc.mentions_refine?
4204
4294
  when Prism::CallNode
@@ -10423,10 +10513,11 @@ module Rigor
10423
10513
 
10424
10514
  # #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
10425
10515
  # census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
10516
+ # Issue #2079 — and each `[:own_write_type, class, ivar, value]` entry, which {IvarContributions} reads.
10426
10517
  def attribute_own_writes(census, path)
10427
- return census unless census.any? { |entry| entry.first == :own_write }
10518
+ return census unless census.any? { |entry| PATH_TAGGED_KINDS.include?(entry.first) }
10428
10519
 
10429
- census.to_set { |entry| entry.first == :own_write ? [*entry, path].freeze : entry }
10520
+ census.to_set { |entry| PATH_TAGGED_KINDS.include?(entry.first) ? [*entry, path].freeze : entry }
10430
10521
  end
10431
10522
 
10432
10523
  # Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
@@ -10859,7 +10950,7 @@ module Rigor
10859
10950
  # A {DYNAMIC_TARGET_PREFIX} key is not a name and never publishes; it is expanded by
10860
10951
  # {#apply_dynamic_target_writes} into a retraction over the names it could have written.
10861
10952
  #
10862
- # @return `[{name => Type}, {name => Set[path]}]`.
10953
+ # @return the value table and the table of source paths per name.
10863
10954
  def finalize_constant_writes(writes)
10864
10955
  values = {}
10865
10956
  sources = {}
@@ -9,6 +9,7 @@ require_relative "../type"
9
9
  require_relative "guard_rebinding"
10
10
  require_relative "project_method_ownership"
11
11
  require_relative "refined_dispatch/own_method"
12
+ require_relative "../run_scoped_slots"
12
13
 
13
14
  module Rigor
14
15
  module Inference
@@ -85,7 +86,9 @@ module Rigor
85
86
  :to_r, :to_c, :to_proc, :to_path, :to_sym, :to_io, :coerce, :exception, :divmod, :each, :respond_to?,
86
87
  :respond_to_missing?, :method_missing, :initialize_copy, :marshal_dump, :pretty_print
87
88
  ].freeze
88
- private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES
89
+ # Issue #2203 — the classes and module every project class's instances reach past its own chain.
90
+ IMPLICIT_ROOTS = %w[Object Kernel BasicObject].freeze
91
+ private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES, :IMPLICIT_ROOTS
89
92
 
90
93
  # Raised inside a proof to abandon it.
91
94
  class Unprovable < StandardError
@@ -113,6 +116,32 @@ module Rigor
113
116
  nil
114
117
  end
115
118
 
119
+ DEFINITION_COUNTS_KEY = RunScopedSlots.register(:__rigor_self_call_ivar_writes_definition_counts__)
120
+ private_constant :DEFINITION_COUNTS_KEY
121
+
122
+ # `{[owner, name] => the number of instance `def` rows of name the project records for owner}`, over every
123
+ # file of `deferred_ranges` (a `discovered_deferred_ranges` table). Built once per table and kept in ONE
124
+ # thread slot keyed by the table's identity (#2300): the table is never written in place, a file whose rows
125
+ # equal the seed's keeps the seed's table, and a scan of every file's rows per query was the cost once the
126
+ # table held the whole project. The slot holds one table and its counts, the next table replaces them, and the
127
+ # run that filled it releases it when it ends ({RunScopedSlots}, #2399).
128
+ def definition_counts(deferred_ranges)
129
+ slot = Thread.current[DEFINITION_COUNTS_KEY]
130
+ return slot[1] if slot && slot[0].equal?(deferred_ranges)
131
+
132
+ counts = {}
133
+ deferred_ranges.each_value do |rows|
134
+ rows.each do |row|
135
+ next unless row[3] == :instance
136
+
137
+ key = [row[4], row[2]]
138
+ counts[key] = counts.fetch(key, 0) + 1
139
+ end
140
+ end
141
+ Thread.current[DEFINITION_COUNTS_KEY] = [deferred_ranges, counts]
142
+ counts
143
+ end
144
+
116
145
  # One proof's state: the instance variables read code writes, and the `def` bodies already read.
117
146
  class Proof
118
147
  attr_reader :written
@@ -128,7 +157,7 @@ module Rigor
128
157
  # Reads the method an implicit-self or `self.` call to `call_node`'s name runs.
129
158
  def self_call(call_node)
130
159
  name = call_node.name
131
- unprovable! if GuardRebinding.code_running_name?(name) || refined?(name)
160
+ unprovable! if GuardRebinding.code_running_name?(name) || refined?(name, on_self: true)
132
161
 
133
162
  if ProjectMethodOwnership.defines?(@class_name, name, :instance, @scope)
134
163
  read_def(proven_def(name))
@@ -185,19 +214,66 @@ module Rigor
185
214
 
186
215
  private
187
216
 
188
- # Whether a `refine` block anywhere in the project defines `name`, on any class: a `using` in effect where the
189
- # read code sits may route the call to it (`refine Kernel do def warn …`), whatever the receiver.
217
+ # Whether a `refine` block in the project defines `name` where the call may reach it: a `using` in effect
218
+ # where the read code sits may route the call to it (`refine Kernel do def warn …`).
190
219
  #
191
220
  # Issue #2163 — the answer reads every file's refinements whole, and a refined class off the chain (`Kernel`,
192
221
  # `IO`, `String`) is no `mark:` the chain read files, so it records `refinement:<name>` and `refinement:*`
193
222
  # whichever way it answers, as `InEffectRefinements` consumers do: a file that starts or stops refining the
194
223
  # name re-checks the reader.
195
- def refined?(name)
224
+ #
225
+ # Issue #2203 — for a call on `self` (`on_self:`) whose reach {#self_reach} knows, a row of the name counts only
226
+ # on a class key in that reach, on the wildcard class key, or on a key the walk could not name
227
+ # (`InEffectRefinements.class_wildcard_key?`); a names-wildcard row (a computed `define_method`,
228
+ # `import_methods`) counts as the name, but only on a class key in the reach. A refinement of `String` leaves
229
+ # a `Holder` alone: dropping the proof is no safer, since the intervening-call reset restores the class-ivar
230
+ # seed and a class guard's narrowing then reports. Where the reach is unknown, and for a call on another
231
+ # receiver, a row of the name counts on any class and a names-wildcard row never does, as before the issue.
232
+ def refined?(name, on_self: false)
233
+ wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
196
234
  if Analysis::DependencyRecorder.active?
197
235
  Analysis::DependencyRecorder.read_name(:refinement, name)
198
- Analysis::DependencyRecorder.read_name(:refinement, Scope::DiscoveryIndex::REFINEMENT_WILDCARD)
236
+ Analysis::DependencyRecorder.read_name(:refinement, wildcard)
237
+ end
238
+ reach = on_self ? self_reach : nil
239
+ @scope.discovered_refinements.any? do |refined, methods|
240
+ next methods.key?(name) if reach.nil?
241
+
242
+ if reach.include?(refined)
243
+ methods.key?(name) || methods.key?(wildcard)
244
+ else
245
+ methods.key?(name) && InEffectRefinements.class_wildcard_key?(@scope, refined)
246
+ end
199
247
  end
200
- @scope.discovered_refinements.any? { |_refined, methods| methods.key?(name) }
248
+ end
249
+
250
+ # Issue #2203 — the classes and modules on the instance chain of `self`'s class and of `Object`, `Kernel` and
251
+ # `BasicObject` (which holds the modules the project mixes into them), with `Object`'s RBS ancestors
252
+ # (`PP::ObjectMixin`); nil when a chain is cut, mixes in a module the tables cannot name, ends in a superclass
253
+ # they do not know (`class Holder < Struct.new(:a)`), or holds an external entry. A project subclass's chain
254
+ # needs no read: {#no_protocol_override} gives the proof up on a refined class or module anywhere on it. The
255
+ # implicit roots' chains are recorded, so a file that mixes a module into `Object` re-checks the reader;
256
+ # `self`'s own chain is recorded by {#no_protocol_override}'s read.
257
+ def self_reach
258
+ return @self_reach if defined?(@self_reach)
259
+
260
+ @self_reach = read_self_reach
261
+ end
262
+
263
+ def read_self_reach
264
+ names = Set.new
265
+ [@class_name.to_s, *IMPLICIT_ROOTS].each do |class_name|
266
+ chain = Scope::ResolutionChain.for(@scope, class_name, :instance, :methods)
267
+ chain.record(@scope) if Analysis::DependencyRecorder.active?
268
+ return nil if chain.truncated? || chain.wildcard_mixin? || chain.opaque_tail?(@scope)
269
+ return nil if chain.entries.any?(&:external?)
270
+
271
+ chain.entries.each { |entry| names << entry.name }
272
+ end
273
+ loader = @scope.environment&.rbs_loader
274
+ return nil if loader.nil?
275
+
276
+ names.merge(loader.ancestor_names_for("Object"))
201
277
  end
202
278
 
203
279
  def call_receiver(node) = node.is_a?(Prism::CallNode) ? node.receiver : nil
@@ -268,11 +344,7 @@ module Rigor
268
344
  end
269
345
 
270
346
  def definition_count(owner, name)
271
- count = 0
272
- @scope.discovered_deferred_ranges.each_value do |rows|
273
- count += rows.count { |row| row[2] == name && row[3] == :instance && row[4] == owner }
274
- end
275
- count
347
+ SelfCallIvarWrites.definition_counts(@scope.discovered_deferred_ranges).fetch([owner, name], 0)
276
348
  end
277
349
 
278
350
  # Whether `chain`'s own order stands with no second world: no order mark and no fork. `ResolutionChain#settle`