rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -6,6 +6,7 @@ require_relative "../source/constant_path"
6
6
  require_relative "../source/node_children"
7
7
  require_relative "refine_census"
8
8
  require_relative "refine_self"
9
+ require_relative "../typing_state"
9
10
  require_relative "in_effect_refinements/proc_literals"
10
11
  require_relative "in_effect_refinements/scope_reads"
11
12
  require_relative "in_effect_refinements/refine_defs"
@@ -53,6 +54,12 @@ module Rigor
53
54
  #
54
55
  # Built lazily: the walk runs the first time a consumer asks, so a file nobody asks about pays one small object.
55
56
  class InEffectRefinements
57
+ # ADR-129 G4 — {#build} fills every one of these once, from the file's root node alone.
58
+ TypingState.memo_field(self, :@built, :@activations, :@refinement_defs, :@refine_defs, :@chained_refined_calls,
59
+ :@class_body_refines, :@refine_contexts, :@literal_bindings, :@refine_offsets,
60
+ :@unresolved_using, :@nesting,
61
+ reason: "built once by #build from the file's root node alone")
62
+
56
63
  # The marker an activation contributes when it may put any refinement in effect: a non-constant `using`, a
57
64
  # `using` whose include expansion is unknown, or a `refine` block whose module this walk cannot name. A list
58
65
  # that carries it answers "any refinement may be in effect"; its position carries no meaning.
@@ -9,8 +9,7 @@ require_relative "unknown_store_widening"
9
9
  module Rigor
10
10
  module Inference
11
11
  # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap surfaced by the Redmine
12
- # 6.1.2 `Query#as_params` survey (ROADMAP § Future cycles / Type-language / engine —
13
- # "Indexed-collection narrowing through `Hash[k] ||= default`").
12
+ # 6.1.2 `Query#as_params` survey.
14
13
  #
15
14
  # After `receiver[key] ||= default` the next read at `receiver[key]` is known non-nil, but
16
15
  # Rigor types each `Hash#[]` independently and the subsequent `<<` / `[]=` / other mutator
@@ -34,8 +33,10 @@ module Rigor
34
33
  #
35
34
  # **Invalidation.** Three conditions drop a recorded narrowing:
36
35
  # - The receiver variable is rebound (handled inside `Scope#with_local` / `Scope#with_ivar`).
37
- # - An intervening `receiver[key] = value` writes the same slot — `:[]=` could rebind the
38
- # slot to nil; conservative drop.
36
+ # - An intervening store writes the same slot — `receiver[key] = value`, an index target, or a
37
+ # compound `receiver[key] op= value` / `&&=` (issue #1218) — since it could rebind the slot to
38
+ # nil; conservative drop. A store whose index is not one literal key drops every narrowing
39
+ # on the receiver.
39
40
  # - An intervening mutator from {MutationWidening::SHAPE_MUTATORS} runs against the receiver
40
41
  # (e.g. `params.delete(:f)`, `params.clear`, `params.default = 0`, `buf.delete_prefix!("x")`).
41
42
  #
@@ -139,14 +140,61 @@ module Rigor
139
140
  MutationWidening::SHAPE_MUTATORS.include?(method_name)
140
141
  end
141
142
 
142
- def invalidate_indexed_write(call_node, current_scope)
143
- args = call_node.arguments&.arguments
144
- return current_scope if args.nil? || args.empty?
143
+ # Drops the narrowing on the slot a store through `[]=` overwrote. `node` is the `[]=` call, an index target
144
+ # (a multi-assign slot, a `for` index, a rescue reference) or a compound index write (`h[k] += v`,
145
+ # `h[k] &&= v`, `h[k] ||= v`); every form reads only `receiver` and `arguments`, and only a `[]=` call's last
146
+ # argument is the stored value rather than an index.
147
+ #
148
+ # Issue #1218 — a literal single index drops that one slot and leaves the others. Any other index list on a
149
+ # stable receiver — a key that is not a literal (`h[k] = 1` may be `h[:e] = 1`), a multi-index splice
150
+ # (`s[0, 1] = [1, 2]` shifts every later slot), a splat — may write any slot, so it drops every narrowing on
151
+ # the receiver, as {.invalidate_mutator} does. So does a negative Integer index on a receiver not shown to be a
152
+ # `Hash`: on an `Array` `a[-1]` is the slot some non-negative index also names. An unstable receiver records
153
+ # no narrowing to drop.
154
+ def invalidate_indexed_write(node, current_scope)
155
+ receiver = stable_receiver(node.receiver)
156
+ return current_scope if receiver.nil?
145
157
 
146
- address = stable_address(call_node.receiver, args.first)
147
- return current_scope if address.nil?
158
+ indexes = index_arguments(node)
159
+ address = indexes.size == 1 ? stable_address(node.receiver, indexes.first) : nil
160
+ if address && !(negative_index?(address.last) && !hash_receiver?(current_scope.type_of(node.receiver)))
161
+ return current_scope.without_indexed_narrowing(*address)
162
+ end
163
+
164
+ current_scope.without_indexed_narrowings_for(*receiver)
165
+ end
166
+
167
+ # Whether the literal `key` may name the same slot as another literal key: a negative Integer, unless the
168
+ # receiver is a `Hash`, which tells `-1` from every other key.
169
+ def aliasing_index?(key, receiver_type)
170
+ negative_index?(key) && !hash_receiver?(receiver_type)
171
+ end
172
+
173
+ def negative_index?(key) = key.is_a?(Integer) && key.negative?
174
+
175
+ def hash_receiver?(receiver_type) = carrier_receiver_type?(receiver_type, HASH_CARRIER)
176
+
177
+ VERBATIM_STORE_CARRIERS = %w[Hash Array].freeze
178
+ HASH_CARRIER = %w[Hash].freeze
179
+ private_constant :VERBATIM_STORE_CARRIERS, :HASH_CARRIER
180
+
181
+ # Whether a store through `[]=` on a receiver of `receiver_type` at the literal `key` leaves exactly the stored
182
+ # value at that slot, so a recorded narrowing may claim it. Only a `Hash` or `Array` carrier (a shape, a tuple,
183
+ # a `Hash` / `Array` nominal, refined or not) does, for every member of a union. A `String`'s `[]=` splices
184
+ # (`s[0] = "xy"` leaves `s[0] == "x"`), and a project class's `[]=` may store anything. A negative Integer key
185
+ # aliases a non-negative one on an `Array` ({.aliasing_index?}), so it records only on a `Hash`.
186
+ def verbatim_slot?(receiver_type, key)
187
+ carrier_receiver_type?(receiver_type, VERBATIM_STORE_CARRIERS) && !aliasing_index?(key, receiver_type)
188
+ end
189
+
190
+ def carrier_receiver_type?(type, classes)
191
+ members = type.is_a?(Type::Union) ? type.members : [type]
192
+ members.all? { |member| classes.include?(UnknownStoreWidening.carrier_class(member)) }
193
+ end
148
194
 
149
- current_scope.without_indexed_narrowing(*address)
195
+ def index_arguments(node)
196
+ args = node.arguments&.arguments || []
197
+ node.is_a?(Prism::CallNode) ? args[0...-1] : args
150
198
  end
151
199
 
152
200
  def invalidate_mutator(call_node, current_scope)
@@ -206,11 +254,9 @@ module Rigor
206
254
  args&.size == 1 ? stable_address(node.receiver, args.first) : nil
207
255
  end
208
256
 
209
- # Companion invalidator for single-hop method-chain narrowings (ROADMAP § Future cycles —
210
- # "Method-call receiver narrowing across stable receivers", B2 from the slice's design
211
- # notes). Drops every `(receiver, *)` chain narrowing rooted at the call's OUTER stable
212
- # receiver — matching the ROADMAP's "any intervening method call against the same
213
- # receiver" criterion. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
257
+ # Companion invalidator for single-hop method-chain narrowings. Drops every `(receiver, *)`
258
+ # chain narrowing rooted at the call's OUTER stable receiver, for any intervening method call
259
+ # against the same receiver. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
214
260
  # stable root) does NOT drop narrowings keyed on `x`, so the worked-site `x.last << y`
215
261
  # pattern correctly preserves the chain narrowing for any further `x.last` read in the same
216
262
  # body. Always-safe (only forgets; never invents).
@@ -19,8 +19,8 @@ module Rigor
19
19
  # the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
20
20
  # them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
21
21
  module InterveningIvarReset
22
- # Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class or
23
- # module body, where `@x` is the class's own instance variable.
22
+ # Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class,
23
+ # module or `class << …` body, where `@x` is the class's own instance variable.
24
24
  NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
25
25
  # Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
26
26
  # method's block, and `super` the superclass's method on `self`.
@@ -180,12 +180,17 @@ module Rigor
180
180
  # takes the mark when widened, so a `nil` only the seed brings back is not fuel for
181
181
  # `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
182
182
  # (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
183
- def widen(scope, mark: false)
183
+ # Issue #2039 — `mark: :unproven` marks as `mark: true` does only when the block answers nil (no write set is
184
+ # proven) and `direct_writes` (called at most once, answering a `Set`) does not name the variable: a call
185
+ # proven to write it, or whose callee visibly assigns it a possibly-nil value ({DirectIvarWrites}), widens it
186
+ # unmarked.
187
+ def widen(scope, mark: false, direct_writes: nil)
184
188
  seed = class_ivar_seed(scope)
185
189
  return scope if seed.nil? || seed.empty?
186
190
 
187
191
  computed = false
188
192
  writes = nil
193
+ direct = lazy_set(direct_writes)
189
194
  seed.reduce(scope) do |widened, (name, seed_type)|
190
195
  local_type = scope.ivar(name)
191
196
  next widened if local_type.nil? || local_type == seed_type
@@ -198,11 +203,28 @@ module Rigor
198
203
 
199
204
  joined = Type::Combinator.union(local_type, seed_type)
200
205
  widened = widened.with_ivar(name, joined)
201
- marked = (mark && nil_free?(local_type)) || scope.declaration_sourced?(:ivar, name)
206
+ marked = widen_marks?(mark, writes, direct, name, local_type) || scope.declaration_sourced?(:ivar, name)
202
207
  marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
203
208
  end
204
209
  end
205
210
 
211
+ # Whether {.widen} stamps the declaration mark on `name`'s binding, narrowed to `local_type`: one whose
212
+ # narrowing ruled `nil` out, under `mark: true`, or under `mark: :unproven` when no write set is proven
213
+ # (`writes` nil) and the callee does not visibly write it (`direct`).
214
+ def widen_marks?(mark, writes, direct, name, local_type)
215
+ return false unless mark && nil_free?(local_type)
216
+
217
+ mark != :unproven || (writes.nil? && !direct.call.include?(name))
218
+ end
219
+ private_class_method :widen_marks?
220
+
221
+ # A callable answering `callable`'s `Set`, computed on the first call; an empty one when `callable` is nil.
222
+ def lazy_set(callable)
223
+ value = nil
224
+ -> { value ||= callable ? callable.call : Set.new }
225
+ end
226
+ private_class_method :lazy_set
227
+
206
228
  # Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
207
229
  # candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
208
230
  # so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "prism"
4
4
 
5
+ require_relative "census_foreign_block"
6
+ require_relative "receiver_writer_call"
7
+
5
8
  module Rigor
6
9
  module Inference
7
10
  # Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
@@ -22,6 +25,17 @@ module Rigor
22
25
  # - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
23
26
  # {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
24
27
  # file writes the same ivar of the same class.
28
+ # - `[:own_write_type, class_name, name, value]` — issues #2079 / #2170: the same write with its coarse value, a
29
+ # literal's as `[:lit, value]` and anything else as {DYNAMIC}, and a class-body `attr_writer` /
30
+ # `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. Issue #2488: an
31
+ # explicit-receiver `E.attr_writer` / `E.attr_accessor` (or one through `send`) records the same {DYNAMIC}
32
+ # entry under `E`, and an `E.define_method` block is an instance body of `E` ({ReceiverWriterCall}). The
33
+ # class-ivar census of a file joins another file's entries, and those of the modules and superclasses on the
34
+ # class's chain, into the ivars it already holds ({ScopeIndexer::IvarContributions}).
35
+ # Issue #2983: a barred `class_eval`-family block on a receiver rooted at another constant files the code in it
36
+ # that runs on an instance under that constant's candidate key ({#remember_root_constant}), a String
37
+ # `class_eval` records a {DYNAMIC} write of {ANY_IVAR} ({#record_string_eval}), and a constant write `C = K` /
38
+ # `C = K.new` records `[:binds_class, key, "C"]` ({#record_binding}), so the join can take `C`'s entries for `K`.
25
39
  class IvarWriterCensus
26
40
  SEND_CALLS = %i[send __send__ public_send].to_set.freeze
27
41
  # The methods a `send` reaching which is an open-world writer.
@@ -34,6 +48,14 @@ module Rigor
34
48
  attr_writer attr_accessor send public_send __send__
35
49
  ].to_set.freeze
36
50
  NO_ARGUMENTS = [].freeze
51
+ # Issue #2079 — the coarse value of a write that is not a literal.
52
+ DYNAMIC = :dynamic
53
+ # Issue #2983 — the ivar name of a write whose ivar no literal spells: a string `class_eval`'s code may write any.
54
+ ANY_IVAR = :*
55
+ # The `class_eval`-family calls that take code in a String.
56
+ STRING_EVALS = %i[class_eval module_eval].to_set.freeze
57
+ # The writer declarations a class body records as a {DYNAMIC} write of each name it spells.
58
+ WRITER_DECLARATIONS = %i[attr_writer attr_accessor].to_set.freeze
37
59
 
38
60
  # The call names any rule here reads, so every other call returns at one Set probe.
39
61
  # (`define_method` is in {SINGLETON_CLASS_CALLS}.)
@@ -44,16 +66,23 @@ module Rigor
44
66
  @instance_ranges = []
45
67
  @owner_names = {}.compare_by_identity
46
68
  @own_writes = {}
69
+ @typed_writes = Set.new
70
+ @rooted = Set.new.compare_by_identity
47
71
  end
48
72
 
49
73
  # Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
50
74
  # or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
51
- # instance (`class << self`, the top level, an opaque block).
52
- def visit(node, owner)
75
+ # instance (`class << self`, the top level, an opaque block). `receiver_owner` is the class an explicit-receiver
76
+ # writer call ({ReceiverWriterCall}) names, resolved by the host walk as it resolves a `class_eval` receiver.
77
+ def visit(node, owner, receiver_owner = nil)
53
78
  case node
54
- when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
79
+ when Prism::DefNode then remember_host(node, owner) if node.receiver.nil?
80
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode then record_binding(node, owner)
55
81
  when Prism::SingletonClassNode then visit_singleton_class(node)
56
- when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
82
+ when Prism::CallNode
83
+ bar_foreign_block(node)
84
+ visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
85
+ record_receiver_writers(node, receiver_owner) if receiver_owner
57
86
  when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
58
87
  Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
59
88
  record_own_write(node) unless @instance_ranges.empty?
@@ -67,6 +96,7 @@ module Rigor
67
96
  owner = instance_owner(node)
68
97
  return if owner.nil?
69
98
 
99
+ record_typed_write(owner, node.name, coarse_value(node))
70
100
  seen = (@own_writes[owner] ||= {})
71
101
  return if seen[node.name]
72
102
 
@@ -74,10 +104,119 @@ module Rigor
74
104
  @census << [:own_write, owner_name(owner), node.name].freeze
75
105
  end
76
106
 
107
+ def record_typed_write(owner, name, value)
108
+ entry = [:own_write_type, owner_name(owner), name, value].freeze
109
+ @census << entry if @typed_writes.add?(entry)
110
+ end
111
+
112
+ # Issue #2079 — a literal's value as `[:lit, value]`; any other value, an operator write and a multiple
113
+ # assignment target as {DYNAMIC}.
114
+ def coarse_value(node)
115
+ value = node.value if node.respond_to?(:value) && !node.is_a?(Prism::InstanceVariableOperatorWriteNode)
116
+ case value
117
+ when Prism::NilNode then [:lit, nil].freeze
118
+ when Prism::TrueNode then [:lit, true].freeze
119
+ when Prism::FalseNode then [:lit, false].freeze
120
+ when Prism::IntegerNode, Prism::FloatNode then [:lit, value.value].freeze
121
+ when Prism::SymbolNode, Prism::StringNode then literal_text(value)
122
+ else DYNAMIC
123
+ end
124
+ end
125
+
126
+ def literal_text(node)
127
+ text = node.unescaped
128
+ return DYNAMIC unless text.valid_encoding?
129
+
130
+ [:lit, node.is_a?(Prism::SymbolNode) ? text.to_sym : text.dup.freeze].freeze
131
+ end
132
+
133
+ # Issue #2490 — a block inside an instance body whose `self` is another object ({CensusForeignBlock}) is
134
+ # remembered with no owner, so its writes credit nothing. Pre-order, so an instance body nested in it (an
135
+ # `E.define_method` block, remembered after) still answers for its own class.
136
+ def bar_foreign_block(node)
137
+ return if @instance_ranges.empty? || !CensusForeignBlock::CALLS.include?(node.name)
138
+
139
+ instance = instance_owner(node)
140
+ return unless instance && CensusForeignBlock.foreign?(node, owner_name(instance))
141
+
142
+ remember(node.block.location, nil)
143
+ remember_reentry(node, instance)
144
+ remember_root_constant(node, instance)
145
+ end
146
+
147
+ # Issue #2983 — the code a barred `class_eval`-family block on a receiver rooted at another constant reenters
148
+ # (`Anc.class_eval`, `INST.class.class_eval`, `AliasK.new.class.class_eval`), remembered under that constant's
149
+ # candidate key ({CensusForeignBlock.root_constant}), in place of the host walk's as-written owner. A class
150
+ # whose census joins the entries of the constant the key resolves to, an ancestor's, an alias's or an instance
151
+ # constant's ({ScopeIndexer::IvarContributions}), then sees the write.
152
+ def remember_root_constant(node, instance)
153
+ root, blocks = CensusForeignBlock.rooted_reentry(node, owner_name(instance))
154
+ blocks&.each { |block| remember(block.location, root) }
155
+ @rooted.merge(blocks) if blocks
156
+ end
157
+
158
+ # The host walk's `owner` of a `def` or `define_method` block, unless {#remember_root_constant} filed it already.
159
+ def remember_host(node, owner) = (remember(node.location, owner) if owner && !@rooted.include?(node))
160
+
161
+ # Issue #2983 — `C = K` or `C = K.new` (`.allocate`) records `[:binds_class, key, "C"]`, `K`'s candidate key and
162
+ # `C`'s qualified name ({CensusForeignBlock.binding_entry}), so a class's census also joins the entries filed
163
+ # under an alias or an instance constant of it (`AliasK.class_eval { … }`, `INST.class.class_eval { … }`).
164
+ def record_binding(node, owner)
165
+ entry = CensusForeignBlock.binding_entry(node, nesting(owner))
166
+ @census << entry if entry
167
+ end
168
+
169
+ # The lexical class and module names `owner` spells: the host walk's prefix Array, or a resolved name.
170
+ def nesting(owner) = owner.nil? ? NO_ARGUMENTS : owner_name(owner).split("::")
171
+
172
+ # Issue #2983 — `class_eval` / `module_eval` given a String runs code the census cannot read, which may define
173
+ # a method writing any ivar: a {DYNAMIC} write of {ANY_IVAR} is recorded for the module the receiver may be. That
174
+ # is the class owning the body for an implicit or `self` receiver in a class body, the instance's class for a
175
+ # receiver in an instance body that {CensusForeignBlock.foreign_module?} does not bar, and otherwise the constant
176
+ # the receiver chain is rooted at.
177
+ def record_string_eval(node, owner)
178
+ return unless STRING_EVALS.include?(node.name) && node.block.nil? && node.arguments
179
+
180
+ instance = instance_owner(node) unless @instance_ranges.empty?
181
+ target = CensusForeignBlock.string_eval_target(node, owner_name(instance), owner_name(owner))
182
+ record_typed_write(target, ANY_IVAR, DYNAMIC) if target
183
+ end
184
+
185
+ # The `define_method` blocks in a barred body that run on the instance again, remembered after the bar so they
186
+ # answer for the instance's class ({CensusForeignBlock.reentry_blocks}).
187
+ def remember_reentry(node, instance)
188
+ CensusForeignBlock.reentry_blocks(node, owner_name(instance)).each do |block|
189
+ remember(block.location, instance)
190
+ end
191
+ end
192
+
193
+ # Issue #2488 — `E.attr_writer :w` declares a writer of `@w` on `E`, and `E.define_method(:m) { … }` installs an
194
+ # instance body of `E`, wherever the call sits.
195
+ def record_receiver_writers(node, class_name)
196
+ ReceiverWriterCall.writer_ivars(node).each { |ivar| record_typed_write(class_name, ivar, DYNAMIC) }
197
+ block = ReceiverWriterCall.define_method_block(node)
198
+ remember(block.location, class_name) if block
199
+ end
200
+
201
+ # Issue #2079 — `attr_writer :x` / `attr_accessor :x` in a class body writes `@x` with whatever the caller hands.
202
+ def record_writer_declaration(node, owner)
203
+ return unless owner && WRITER_DECLARATIONS.include?(node.name) && self_receiver?(node)
204
+ # A guard: an instance body's own `attr_writer` call raises (the method is `Module`'s), so only the class
205
+ # body's declares a writer.
206
+ return unless instance_owner(node).nil?
207
+
208
+ (node.arguments&.arguments || NO_ARGUMENTS).each do |argument|
209
+ name = literal_name(argument)
210
+ record_typed_write(owner, :"@#{name}", DYNAMIC) if name
211
+ end
212
+ end
213
+
77
214
  def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
78
215
 
79
216
  def visit_call(node, owner)
80
- remember(node.block.location, owner) if owner && define_method_block?(node)
217
+ record_writer_declaration(node, owner)
218
+ record_string_eval(node, owner)
219
+ remember_host(node.block, owner) if define_method_block?(node)
81
220
  if self_receiver?(node) || own_singleton_class_call?(node)
82
221
  return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
83
222
 
@@ -88,12 +227,19 @@ module Rigor
88
227
  end
89
228
  end
90
229
 
91
- # `class << self` inside an instance `def` opens the instance's singleton class.
230
+ # `class << self` inside an instance `def` opens the instance's singleton class. Its body's `self` is that
231
+ # singleton class, so an `@x` write there is the singleton's own ivar (#2490), whatever the expression.
92
232
  def visit_singleton_class(node)
93
- return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
233
+ return if @instance_ranges.empty?
94
234
 
95
235
  instance = instance_owner(node)
96
- record_class(instance) if instance
236
+ return unless instance
237
+
238
+ record_class(instance) if node.expression.is_a?(Prism::SelfNode)
239
+ return unless node.body
240
+
241
+ remember(node.body.location, nil)
242
+ remember_reentry(node, instance)
97
243
  end
98
244
 
99
245
  def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
@@ -5,6 +5,8 @@ require "prism"
5
5
  require_relative "../type"
6
6
  require_relative "mutation_widening"
7
7
  require_relative "key_presence_guard/scan"
8
+ require_relative "../typing_state"
9
+ require_relative "../memo_audit"
8
10
 
9
11
  module Rigor
10
12
  module Inference
@@ -57,9 +59,22 @@ module Rigor
57
59
  ] + MutationWidening::SHAPE_MUTATORS.to_a
58
60
  ).to_set.freeze
59
61
 
60
- STATE_KEY = :__rigor_key_presence_guard
61
- CACHE_KEY = :__rigor_key_presence_guard_rewalk
62
- private_constant :STATE_KEY, :CACHE_KEY
62
+ # ADR-129 WD1 — the walk {.with_guards} installs is a Frame, and {.record}, which rewrites its `guard_ends` in
63
+ # place, is a Resource touch that also takes a fresh id ({TypingState::State#rewrite}), so a read of the walk's
64
+ # guards is fixed while the id is. The walk's `defs`, `frozen_constants` and `memo` are extensional caches of
65
+ # `root`, filled in place without a touch.
66
+ WALK = TypingState.frame(
67
+ :key_presence_walk,
68
+ contents: :resource, read_exemption: :id_determined,
69
+ reason: "installed per guarded re-walk; each recorded guard rewrites its `guard_ends`",
70
+ witness: "spec/fixtures/typing_state_witnesses/key_presence_walk.rb"
71
+ )
72
+ REWALK = TypingState.memo(
73
+ :key_presence_rewalk,
74
+ run_scoped: true, audit: "key presence rewalk",
75
+ reason: "the last file's guarded index, keyed by the file's root"
76
+ )
77
+ private_constant :WALK, :REWALK
63
78
 
64
79
  # The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
65
80
  Walk = Struct.new(:root, :guard_ends, :defs, :frozen_constants, :memo) do
@@ -87,25 +102,21 @@ module Rigor
87
102
 
88
103
  # Runs the block with guards recorded and read, for the re-walk of `root`. Thread-local, so it holds under a
89
104
  # Ractor worker too.
90
- def with_guards(root)
91
- previous = Thread.current[STATE_KEY]
92
- Thread.current[STATE_KEY] = Walk.new(root, {}, nil, nil, {})
93
- yield
94
- ensure
95
- Thread.current[STATE_KEY] = previous
105
+ def with_guards(root, &)
106
+ TypingState.current.with(WALK, Walk.new(root, {}, nil, nil, {}), &)
96
107
  end
97
108
 
98
109
  # Runs the block with guards off again: a callee's return summary, memoised for the whole run, must be the one
99
110
  # the file's analysis reads.
100
- def without_guards
101
- previous = Thread.current[STATE_KEY]
102
- Thread.current[STATE_KEY] = nil
103
- yield
104
- ensure
105
- Thread.current[STATE_KEY] = previous
111
+ def without_guards(&)
112
+ TypingState.current.with(WALK, nil, &)
106
113
  end
107
114
 
108
- def active? = !Thread.current[STATE_KEY].nil?
115
+ def active? = !TypingState.current[WALK].nil?
116
+
117
+ # {.active?}, read only to decide whether to turn the walk off for the extent that follows ({.without_guards}):
118
+ # that extent reads it off either way, so the key audit does not count the read (ADR-129 WD1).
119
+ def forcing_off? = !TypingState.current.forcing_read(WALK).nil?
109
120
 
110
121
  # Whether `call.possible-nil-receiver` should withhold its report on `call_node`, whose receiver the file's
111
122
  # analysis types `receiver_type`: true only when the guarded re-walk types that receiver as exactly
@@ -129,19 +140,33 @@ module Rigor
129
140
  # The guarded scope index of `root`, built once per file (the cache holds the last file only), or nil when the
130
141
  # file holds no guard-shaped call.
131
142
  def rewalk(root, scope_index)
132
- cached = Thread.current[CACHE_KEY]
133
- return cached.last if cached&.first.equal?(root)
143
+ return guarded_index(root, scope_index) if MemoAudit.enabled? && MemoAudit.suspended?
144
+
145
+ state = TypingState.current
146
+ cached = state[REWALK]
147
+ if cached&.first.equal?(root)
148
+ # A re-walk that raised is stored as nil, and its audit recomputation answers nil the same way.
149
+ return MemoAudit.check("key presence rewalk", cached.last, root) do
150
+ guarded_index(root, scope_index)
151
+ rescue StandardError
152
+ nil
153
+ end
154
+ end
134
155
 
135
156
  # Cached before the walk, so a re-walk that raises is attempted once per file and then answers nil.
136
- Thread.current[CACHE_KEY] = [root, nil]
157
+ state[REWALK] = [root, nil]
158
+ index = MemoAudit.key_audit("key presence rewalk") { guarded_index(root, scope_index) }
159
+ state[REWALK] = [root, index]
160
+ index
161
+ end
162
+
163
+ def guarded_index(root, scope_index)
137
164
  return nil unless guard_shaped_call?(root)
138
165
 
139
166
  base = scope_index[root]
140
- index = base && with_guards(root) do
167
+ base && with_guards(root) do
141
168
  ScopeIndexer.index(root, default_scope: base.with_isolated_side_tables)
142
169
  end
143
- Thread.current[CACHE_KEY] = [root, index]
144
- index
145
170
  end
146
171
 
147
172
  def guard_shaped_call?(node)
@@ -205,7 +230,8 @@ module Rigor
205
230
  # The truthy edge of `receiver.key?(key)` with a non-literal key, inside the re-walk: `scope` with the guard
206
231
  # recorded, or nil when the guard does not apply.
207
232
  def record(call_node, scope)
208
- walk = Thread.current[STATE_KEY]
233
+ state = TypingState.current
234
+ walk = state[WALK]
209
235
  return nil if walk.nil?
210
236
 
211
237
  args = call_node.arguments&.arguments
@@ -216,6 +242,7 @@ module Rigor
216
242
  return nil unless receiver_type?(scope.type_of(call_node.receiver))
217
243
  return nil unless Scan.eligible?(walk, call_node, guard)
218
244
 
245
+ state.rewrite(WALK)
219
246
  walk.guard_ends[guard] = call_node.location.end_offset
220
247
  scope.with_indexed_narrowing(*guard, PRESENT)
221
248
  end
@@ -225,7 +252,7 @@ module Rigor
225
252
  # read may have changed it ({.intervening?}), and no value of the receiver can be `nil` (the read cannot tell a
226
253
  # value's own `nil` from the miss). Nil when no guard applies or nothing would change.
227
254
  def guarded_read(node, type, scope)
228
- walk = Thread.current[STATE_KEY]
255
+ walk = TypingState.current[WALK]
229
256
  guard = walk && held_guard(node, scope)
230
257
  return nil if guard.nil? || Scan.intervening?(walk, guard, node)
231
258
 
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../../source/constant_path"
6
6
  require_relative "../../source/node_children"
7
+ require_relative "../../typing_state"
7
8
 
8
9
  module Rigor
9
10
  module Inference
@@ -50,6 +51,9 @@ module Rigor
50
51
  # `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
51
52
  # (`respond_to?(:gets)`, `send(:gets)`).
52
53
  class SelfEvidence
54
+ TypingState.memo_field(self, :@scan, reason: "one walk of the file's root node, built on first ask")
55
+ TypingState.memo_field(self, :@const_set_spellings, reason: "one walk of the file's root, built on first ask")
56
+
53
57
  # The context of a reader whose `self` is `main`.
54
58
  MAIN = :main
55
59
  UNKNOWN = :unknown
@@ -83,8 +87,14 @@ module Rigor
83
87
  def initialize(root)
84
88
  @root = root
85
89
  @scan = nil
90
+ @const_set_spellings = nil
86
91
  end
87
92
 
93
+ # Issue #3156 — the constants this file binds by a `const_set` with a literal name on the body's own module
94
+ # (implicit `self` or `self.const_set`) inside a `class` / `module` body, qualified by that body's nesting
95
+ # (`module NS; const_set(:AMid, Other)` binds `"NS::AMid"`). The census files no such write.
96
+ def const_set_spellings = (@const_set_spellings ||= collect_const_sets(@root, nil, Set.new).freeze)
97
+
88
98
  # {MAIN}, the `Prism::ClassNode` or `Prism::ModuleNode` whose body the implicit-self reader `call_node` runs
89
99
  # in, or nil: the file may give it another `self`, or it is not one of this file's readers.
90
100
  def context(call_node) = scan.context(call_node)
@@ -96,6 +106,26 @@ module Rigor
96
106
 
97
107
  def scan = (@scan ||= Scan.new(@root))
98
108
 
109
+ def collect_const_sets(node, namespace, found)
110
+ case node
111
+ when Prism::ClassNode, Prism::ModuleNode
112
+ inner = Source::ConstantPath.pushed_nesting(namespace ? [namespace] : [], node.constant_path)&.first
113
+ collect_const_sets(node.body, inner, found) if inner
114
+ return found
115
+ when Prism::CallNode
116
+ found << "#{namespace}::#{literal_const_set_name(node)}" if namespace && literal_const_set_name(node)
117
+ end
118
+ node&.compact_child_nodes&.each { |child| collect_const_sets(child, namespace, found) }
119
+ found
120
+ end
121
+
122
+ def literal_const_set_name(node)
123
+ return nil unless node.name == :const_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
124
+
125
+ name = node.arguments&.arguments&.first
126
+ name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
127
+ end
128
+
99
129
  # One walk of the file, whose answers {SelfEvidence} keeps once the walk is complete.
100
130
  class Scan
101
131
  def initialize(root)
@@ -261,10 +261,13 @@ module Rigor
261
261
  # The scope a block or lambda body enters with, as {MatchRebinding.block_entry} gives the match globals': `$_`
262
262
  # forgotten when the body may set it, which a later iteration then reads, when the frame makes a closure that
263
263
  # may, or when the owning call may: its receiver chain and arguments run before the method yields, and
264
- # `IO.foreach` sets `$_` to each line it yields.
264
+ # `IO.foreach` sets `$_` to each line it yields. Issue #3195 — and whatever the body does when it runs later
265
+ # than where it is written ({FreshFrameBlocks.deferred_body?}): a proc's body, an enumerator's or a hash's
266
+ # default proc reads the `$_` the slot holds when it runs.
265
267
  def block_entry(scope, block_node, call_node = nil)
266
268
  return scope unless scope.last_line_bound?
267
- return scope unless scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
269
+ return scope unless FreshFrameBlocks.deferred_body?(block_node, call_node, scope) ||
270
+ scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
268
271
 
269
272
  scope.forget_last_line
270
273
  end