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
@@ -13,6 +13,7 @@ require_relative "guard_rebinding"
13
13
  require_relative "intervening_ivar_reset"
14
14
  require_relative "key_presence_guard"
15
15
  require_relative "last_line"
16
+ require_relative "match_rebinding"
16
17
  require_relative "operand_effects"
17
18
  require_relative "optimistic_origin"
18
19
  require_relative "project_method_ownership"
@@ -532,7 +533,17 @@ module Rigor
532
533
  lexical_class_name(condition, scope) || case_equality_target_class(condition)
533
534
  end
534
535
  body_scope = record_bot_guard(narrow_guarded_slot(body_scope, slot, truthy_type), slot, truthy_type, classes)
535
- [body_scope, narrow_guarded_slot(scope, slot, falsey_type)]
536
+ marked = unwrap_case_subject(subject).is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(slot.name)
537
+ # A marked subject keeps its mark under any `when` (`when "r"` narrows it without writing it); an unmarked one
538
+ # is stamped only under class conditions.
539
+ if marked || classes.none?(&:nil?)
540
+ static = gradual_union_slot?(slot) ? gradual_static_part(slot.current) : nil
541
+ static &&= case_when_types(scope, static, conditions).first
542
+ body_scope = mark_gradual_narrowing(body_scope, slot, truthy_type, marked: marked, narrowed_static: static)
543
+ end
544
+ falsey_scope = narrow_guarded_slot(scope, slot, falsey_type)
545
+ falsey_scope = falsey_scope.with_gradual_narrowing_mark(slot.name) if marked
546
+ [body_scope, falsey_scope]
536
547
  end
537
548
 
538
549
  CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
@@ -588,12 +599,21 @@ module Rigor
588
599
  end
589
600
  end
590
601
 
591
- # When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
592
- # match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
593
- # truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
594
- # through any of them, so only `$~`/`$&` are safely non-nil; numbered groups whose
595
- # presence differs per alternative stay `String | nil`. With no regex condition the entry
596
- # scope passes through unchanged.
602
+ # When every condition is a `RegularExpressionNode` literal, the body edge narrows the
603
+ # match-data globals as `analyse_regex_match_predicate`'s truthy edge does. With multiple
604
+ # regex conditions (`when /a/, /b/`) the body is reachable through any of them, so only
605
+ # `$~`/`$&` are safely non-nil; numbered groups whose presence differs per alternative stay
606
+ # `String | nil`. With no regex condition the entry scope passes through unchanged.
607
+ #
608
+ # Issue #1474 — a clause that mixes a regex literal with another condition (`when /(a)/,
609
+ # "lit"`) is also entered through the other conditions: each regex literal before them ran
610
+ # its `===` and failed, leaving the globals nil, and a condition before any regex leaves
611
+ # them as they were. The body scope then joins the match globals of every way in rather
612
+ # than claiming the regex matched: each condition contributes the scope its own `===`
613
+ # leaves — a regex literal's truthy edge, the falsey edge of the regex literals that
614
+ # failed before it, the entry state where none ran, and the unknown-match edges for a
615
+ # condition that may itself be or run a Regexp ({MatchRebinding::Operands.pattern_value?},
616
+ # {MatchRebinding.may_match?}).
597
617
  def apply_when_regex_globals(conditions, scope)
598
618
  regexes = conditions.grep(Prism::RegularExpressionNode)
599
619
  return scope if regexes.empty?
@@ -604,8 +624,102 @@ module Rigor
604
624
  else
605
625
  Set.new
606
626
  end
607
- truthy, = regex_match_predicate_scopes(scope, unconditional)
608
- truthy
627
+ truthy, falsey = regex_match_predicate_scopes(scope, unconditional)
628
+ return truthy if regexes.size == conditions.size
629
+
630
+ empty_truthy, empty_falsey = regex_match_predicate_scopes(scope, Set.new)
631
+ failed = scope
632
+ paths = conditions.map do |condition|
633
+ kind = when_regex_condition_kind(condition, scope)
634
+ path, failed = when_regex_condition_scopes(kind, failed, truthy, falsey, empty_truthy, empty_falsey)
635
+ path
636
+ end
637
+ paths.reduce(:join)
638
+ end
639
+
640
+ # How a `when` condition's `===` can touch the match globals, for {#apply_when_regex_globals}:
641
+ # `:literal` a `RegularExpressionNode`, whose match the clause's shared unconditional groups
642
+ # describe; `:interpolated` an interpolated regex literal, a definite match whose groups are
643
+ # unknown; `:may_match` any other condition that may be a Regexp or run one, whose `===`
644
+ # outcome for the globals is unknown; `:neutral` a condition whose `===` and evaluation run
645
+ # no match, leaving the globals as the earlier conditions left them.
646
+ def when_regex_condition_kind(condition, scope)
647
+ return :literal if condition.is_a?(Prism::RegularExpressionNode)
648
+ return :interpolated if condition.is_a?(Prism::InterpolatedRegularExpressionNode)
649
+ if MatchRebinding::Operands.pattern_value?(condition, scope) ||
650
+ MatchRebinding.may_match?(condition, scope)
651
+ return :may_match
652
+ end
653
+
654
+ :neutral
655
+ end
656
+
657
+ # `[body_path_scope, failed_scope]` for one `when` condition of a mixed clause, `failed`
658
+ # being the globals state the conditions before it left. On the path where this condition's
659
+ # `===` matched the body reads the truthy edge (`:literal`), the ungrouped truthy edge
660
+ # (`:interpolated`), the join of that edge with `failed` (`:may_match` — it may not have
661
+ # been a match at all), or `failed` itself (`:neutral`). On the path where it failed the
662
+ # next conditions read the matching falsey edge, the ungrouped one, the join of it with
663
+ # `failed`, or `failed`.
664
+ def when_regex_condition_scopes(kind, failed, truthy, falsey, empty_truthy, empty_falsey)
665
+ case kind
666
+ when :literal then [truthy, falsey]
667
+ when :interpolated then [empty_truthy, empty_falsey]
668
+ when :may_match then [empty_truthy.join(failed), failed.join(empty_falsey)]
669
+ else [failed, failed]
670
+ end
671
+ end
672
+
673
+ # Issue #2443, #1371 — the scope the block of a call that yields only after it has matched in the caller's frame
674
+ # enters with ({MatchRebinding::YieldingMatch}): the match edge of `pattern`, the call's pattern argument, over
675
+ # `scope` with the match globals forgotten. `$~` is a `MatchData`, `$&` and the pre- and post-match a `String`,
676
+ # and so is each numbered group {#yielded_capture_groups} vouches for; every other group is unbound, so it
677
+ # reads the RBS `String?` whatever an earlier match in the method bound it to.
678
+ def yielded_match_scope(scope, pattern)
679
+ regex_match_predicate_scopes(scope, yielded_capture_groups(pattern)).first
680
+ end
681
+
682
+ # The pattern sources {#yielded_capture_groups} reads groups from: literal characters, `.`, the anchors, the
683
+ # quantifiers, `|`, plain and `(?:…)` groups, the class escapes, `\n`, `\r` and `\t`, an escaped punctuation
684
+ # character, and a bracket class holding only those with no bracket inside it. The group walk
685
+ # ({#unconditional_capture_groups}) reads these right; it misreads sources outside them (#2472, #1476).
686
+ PLAIN_PATTERN_ATOM = %r{[A-Za-z0-9 _\-.,:;=<>!@%&~'"`/^$+*?|(){}#]|\\[wWdDsShHnrt]|\\[^A-Za-z0-9\s]}
687
+ PLAIN_PATTERN_CLASS =
688
+ %r{\[\^?(?:[A-Za-z0-9 _\-.,:;=<>!@%~'"`/^$+*?|(){}#]|\\[wWdDsShHnrt]|\\[^A-Za-z0-9\s])+\]}
689
+ PLAIN_PATTERN = /\A(?:#{PLAIN_PATTERN_CLASS}|#{PLAIN_PATTERN_ATOM}|\\[bBAzZG])*\z/
690
+ OTHER_GROUP_FORM = /\(\?(?!:)/
691
+ # A quantifier on a quantifier repeats the quantified atom, so `(a)+{0}` and `(a){2}?` may match without the
692
+ # group. `+?` and `++` are one quantifier.
693
+ STACKED_QUANTIFIER = /\+[?+]?[*{]|\+[?+][?+]|\}[*+?{]/
694
+ NO_GROUPS = Set.new.freeze
695
+ private_constant :PLAIN_PATTERN_ATOM, :PLAIN_PATTERN_CLASS, :PLAIN_PATTERN, :OTHER_GROUP_FORM,
696
+ :STACKED_QUANTIFIER, :NO_GROUPS
697
+
698
+ # Issue #1371 — the 1-based indices of the groups every match of `pattern` sets, read only from a regex
699
+ # literal written between slashes, without interpolation or the `x` flag, whose source is a {PLAIN_PATTERN}
700
+ # with no `(?` form but `(?:` and no {STACKED_QUANTIFIER}. Any other pattern, a String one included, vouches
701
+ # for no group: declining costs precision, while a misread group would read `String` where Ruby has nil. A
702
+ # `%r` literal is declined because its source escapes the delimiter, which hides a quantifier from the walk:
703
+ # `%r,(a){\,1}b,` is `/(a){,1}b/`.
704
+ def yielded_capture_groups(pattern)
705
+ return NO_GROUPS unless pattern.is_a?(Prism::RegularExpressionNode) && !pattern.extended?
706
+ return NO_GROUPS unless pattern.opening == "/"
707
+
708
+ plain_pattern_source?(pattern.content) ? unconditional_capture_groups(pattern.content) : NO_GROUPS
709
+ end
710
+
711
+ def plain_pattern_source?(source)
712
+ source.ascii_only? && PLAIN_PATTERN.match?(source) && !OTHER_GROUP_FORM.match?(source) &&
713
+ !STACKED_QUANTIFIER.match?(source)
714
+ end
715
+ private_class_method :yielded_capture_groups, :plain_pattern_source?
716
+
717
+ # Issue #2443 — true when every member of `type` is a String (a project subclass included) or a `Dynamic` that
718
+ # is untyped or may hold only Strings, Symbols and nil ({#match_member_kind}). A `nil` member never runs the
719
+ # block, so it counts neither way.
720
+ def string_match_receiver?(type, scope)
721
+ kinds = match_member_kinds(type, scope) - [:nil]
722
+ !kinds.empty? && kinds.all? { |kind| %i[string string_subclass dynamic].include?(kind) }
609
723
  end
610
724
 
611
725
  # Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
@@ -616,7 +730,30 @@ module Rigor
616
730
  def analyse(node, scope)
617
731
  edges = analyse_shape(node, scope)
618
732
  reader = last_line_condition(node)
619
- reader ? LastLine.predicate_scopes(reader, scope, edges) : edges
733
+ edges = LastLine.predicate_scopes(reader, scope, edges) if reader
734
+ keep_gradual_marks(node, scope, edges)
735
+ end
736
+
737
+ # Issue #2656 — a guard edge narrows a local; it does not write it. So a local a class guard marked as a gradual
738
+ # narrowing ({Scope#with_gradual_narrowing_mark}) keeps the mark on both edges of every later predicate
739
+ # (`return if x.nil?`, `x != ""`, a second class guard), which bind the narrowed type through `with_local` and
740
+ # drop it. A local the predicate itself writes (`if (x = v)`) loses it, as any write does.
741
+ def keep_gradual_marks(node, scope, edges)
742
+ return edges if edges.nil? || (edges[0].equal?(scope) && edges[1].equal?(scope))
743
+
744
+ names = scope.gradual_narrowing_names
745
+ return edges if names.empty?
746
+
747
+ # Defensive: the statement walk binds a predicate's own write (`if (x = v)`) before its edges are read, so the
748
+ # mark is already gone there; the exclusion keeps a write that reached here from carrying it.
749
+ names -= OperandEffects.written_variables(node).map(&:to_sym)
750
+ return edges if names.empty?
751
+
752
+ edges.map do |edge|
753
+ names.reduce(edge) do |acc, name|
754
+ acc.gradual_narrowing?(name) || acc.local(name).nil? ? acc : acc.with_gradual_narrowing_mark(name)
755
+ end
756
+ end
620
757
  end
621
758
 
622
759
  # The reader call a condition's value is, or nil.
@@ -968,12 +1105,22 @@ module Rigor
968
1105
  end
969
1106
 
970
1107
  def analyse_local_read(node, scope)
971
- current = scope.local(node.name)
1108
+ local_truthiness_edges(node.name, scope)
1109
+ end
1110
+
1111
+ # The falsey edge is where an optimistic lookup's miss lands (ADR-101, issue #2306): `Scope#with_local`
1112
+ # drops the issue #286 mark as a write does, so the edge re-takes the one `scope` holds. Without it the
1113
+ # join after `if v; …; end` with no `else` lost the mark on both arms and read `v` as nil-free, although
1114
+ # a miss reaches that join through the implicit else. The truthy edge proves the value non-nil, so it
1115
+ # keeps dropping the mark.
1116
+ def local_truthiness_edges(name, scope)
1117
+ current = scope.local(name)
972
1118
  return nil if current.nil?
973
1119
 
1120
+ falsey = scope.with_local(name, narrow_falsey(current))
974
1121
  [
975
- scope.with_local(node.name, narrow_truthy(current)),
976
- scope.with_local(node.name, narrow_falsey(current))
1122
+ scope.with_local(name, narrow_truthy(current)),
1123
+ OptimisticOrigin.carry_local_mark(scope, falsey, name, nil)
977
1124
  ]
978
1125
  end
979
1126
 
@@ -987,13 +1134,7 @@ module Rigor
987
1134
  # holds the just-bound value; the narrowing contract on the surrounding `if` is the
988
1135
  # same.
989
1136
  def analyse_local_write(node, scope)
990
- current = scope.local(node.name)
991
- return nil if current.nil?
992
-
993
- [
994
- scope.with_local(node.name, narrow_truthy(current)),
995
- scope.with_local(node.name, narrow_falsey(current))
996
- ]
1137
+ local_truthiness_edges(node.name, scope)
997
1138
  end
998
1139
 
999
1140
  # Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
@@ -1007,9 +1148,11 @@ module Rigor
1007
1148
  current = scope.ivar(node.name)
1008
1149
  return nil if current.nil?
1009
1150
 
1151
+ # The falsey edge keeps the optimistic mark, as {#local_truthiness_edges} explains.
1152
+ falsey = scope.with_ivar(node.name, narrow_falsey(current))
1010
1153
  [
1011
1154
  scope.with_ivar(node.name, narrow_truthy(current)),
1012
- scope.with_ivar(node.name, narrow_falsey(current))
1155
+ OptimisticOrigin.carry_ivar_mark(scope, falsey, node.name, nil)
1013
1156
  ]
1014
1157
  end
1015
1158
 
@@ -1180,15 +1323,6 @@ module Rigor
1180
1323
  refinements
1181
1324
  end
1182
1325
 
1183
- # Recognised CallNode predicates:
1184
- # - `recv.nil?` (Slice 6 phase 1, no args, no block)
1185
- # - unary `!recv` (`name == :!`, no args, no block)
1186
- # - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
1187
- # static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
1188
- # - `local == literal` / `literal == local` and the `!=` mirror for trusted static
1189
- # literals (Slice 6 phase 2 sub-phase 2).
1190
- # Anything else returns nil so the surrounding analyser falls through to the
1191
- # no-narrowing fallback.
1192
1326
  # Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
1193
1327
  # it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
1194
1328
  # variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
@@ -1798,7 +1932,7 @@ module Rigor
1798
1932
 
1799
1933
  def match_dynamic_kind(member, scope)
1800
1934
  facet = member.static_facet
1801
- return :dynamic if facet.is_a?(Type::Top)
1935
+ return :dynamic if Type::Combinator.untyped?(member)
1802
1936
 
1803
1937
  facet_kinds = match_member_kinds(facet, scope)
1804
1938
  :dynamic if facet_kinds.all? { |kind| (STRING_MATCH_KINDS + [:nil]).include?(kind) }
@@ -1960,7 +2094,7 @@ module Rigor
1960
2094
 
1961
2095
  # Returns the Set of 1-based numbered-capture indices that are UNCONDITIONAL in
1962
2096
  # `source`: present on every successful match because no optional quantifier (`?`,
1963
- # `*`, `{0,…}`) applies to the group or any ancestor group, and the pattern contains no
2097
+ # `*`, `{0,…}`, `{,…}`) applies to the group or any ancestor group, and the pattern contains no
1964
2098
  # alternation (`|`). Optional and alternation-reachable groups are excluded — at
1965
2099
  # runtime `$N` is `nil` for them even when the overall match succeeds, so narrowing
1966
2100
  # them to non-nil `String` would be unsound. The walker is intentionally light (char
@@ -2044,13 +2178,15 @@ module Rigor
2044
2178
  end
2045
2179
 
2046
2180
  # True when the group whose closing paren is at `source[pos]` is followed by a
2047
- # quantifier that permits zero repetitions (`?`, `*`, `{0…}`). `+` and `{1,…}` do NOT
2048
- # make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still zero-permitting
2049
- # on the base quantifier.
2181
+ # quantifier that permits zero repetitions (`?`, `*`, `{0…}`, `{,…}`). `+` and `{1,…}`
2182
+ # do NOT make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still
2183
+ # zero-permitting on the base quantifier. `{,…}` counts only with a digit after the
2184
+ # comma — `{,}`, `{,x}` and `{, }` are literal braces, not quantifiers.
2050
2185
  def next_quantifier_optional?(source, pos)
2051
2186
  case source[pos]
2052
2187
  when "?", "*" then true
2053
- when "{" then source[pos + 1] == "0"
2188
+ when "{" then source[pos + 1] == "0" ||
2189
+ (source[pos + 1] == "," && source[pos + 2].to_s.match?(/\d/))
2054
2190
  else false
2055
2191
  end
2056
2192
  end
@@ -2466,10 +2602,10 @@ module Rigor
2466
2602
  narrowed == original ? :relational : :local_binding
2467
2603
  end
2468
2604
 
2469
- # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
2470
- # MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
2605
+ # `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The argument
2471
2606
  # MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
2472
- # qualified class name. Anything else falls through to "no narrowing".
2607
+ # qualified class name. The receiver is a local, a global, constant or instance variable
2608
+ # ({#receiver_slot}) or a single-hop chain; anything else falls through to "no narrowing".
2473
2609
  def analyse_class_predicate(node, scope, exact:)
2474
2610
  return nil if node.arguments.nil?
2475
2611
  return nil unless node.arguments.arguments.size == 1
@@ -2519,7 +2655,62 @@ module Rigor
2519
2655
  truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
2520
2656
  falsey = narrow_not_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
2521
2657
  truthy_scope = record_bot_guard(narrow_guarded_slot(scope, slot, truthy), slot, truthy, [class_name])
2522
- [truthy_scope, narrow_guarded_slot(scope, slot, falsey)]
2658
+ static = gradual_union_slot?(slot) ? gradual_static_part(slot.current) : nil
2659
+ static &&= narrow_class(static, class_name, exact: exact, environment: environment, scope: scope)
2660
+ [mark_gradual_narrowing(truthy_scope, slot, truthy, narrowed_static: static),
2661
+ narrow_guarded_slot(scope, slot, falsey)]
2662
+ end
2663
+
2664
+ # Issue #2656 — a class guard's truthy edge over a local that held a `Dynamic` value marks the narrowed binding
2665
+ # ({Scope#with_gradual_narrowing_mark}): the binding keeps the asked class's `Nominal`, which dispatch and the
2666
+ # method rules read, while `call.argument-type-mismatch` judges it as the gradual value it came from, which a
2667
+ # literal-typed parameter (`("r" | "w")`) accepts. Only a local is marked, and only when the edge left it
2668
+ # something other than `Dynamic` or `bot`.
2669
+ def mark_gradual_narrowing(scope, slot, narrowed, marked: false, narrowed_static: nil)
2670
+ return scope unless slot.kind == :local
2671
+
2672
+ mark_gradual_local(scope, slot.name, slot.current, narrowed, marked: marked, narrowed_static: narrowed_static)
2673
+ end
2674
+
2675
+ # {#mark_gradual_narrowing} for the local `name`, which held `current` before the guard and `narrowed` after.
2676
+ # `current` is gradual when it is `Dynamic` or a union with a `Dynamic` member (`h[:m] || "r"`, `c ? h[:m] :
2677
+ # nil`); `marked` says the local already carried the mark, so a `case` or a capture over it keeps it (a
2678
+ # predicate keeps it through {.keep_gradual_marks}).
2679
+ def mark_gradual_local(scope, name, current, narrowed, marked: false, narrowed_static: nil)
2680
+ return scope unless marked || gradual_bearing?(current)
2681
+ return scope if narrowed.nil? || narrowed.is_a?(Type::Dynamic) || narrowed.is_a?(Type::Bot)
2682
+ return scope unless marked || literal_or_empty?(narrowed_static || gradual_static_part(current))
2683
+
2684
+ scope.with_gradual_narrowing_mark(name)
2685
+ end
2686
+ public :mark_gradual_local
2687
+
2688
+ def gradual_bearing?(type)
2689
+ type.is_a?(Type::Dynamic) || (type.is_a?(Type::Union) && type.members.any?(Type::Dynamic))
2690
+ end
2691
+
2692
+ # Issue #2656 — the non-`Dynamic` members of a union `current` (nil for any other type). A union is stamped
2693
+ # only when that part, narrowed by the guard (`narrowed_static`, or the part itself where the caller cannot
2694
+ # narrow it), is literals or nothing: the rule judges a literal member as written, so the mark then withholds
2695
+ # only for what came from the gradual member. A static member that narrows to a class (`d = "a" + s.to_s;
2696
+ # x = h[:m] || d`) would be read past as if it were gradual, so such a union is not stamped.
2697
+ def gradual_static_part(current)
2698
+ return nil unless current.is_a?(Type::Union)
2699
+
2700
+ Type::Combinator.union(*current.members.grep_v(Type::Dynamic))
2701
+ end
2702
+
2703
+ # Whether `slot` is a local holding a union with a `Dynamic` member, the one case {.gradual_static_part}
2704
+ # answers for; checked first so a class guard on any other slot builds nothing.
2705
+ def gradual_union_slot?(slot)
2706
+ slot.kind == :local && slot.current.is_a?(Type::Union) && slot.current.members.any?(Type::Dynamic)
2707
+ end
2708
+
2709
+ def literal_or_empty?(type)
2710
+ return true if type.nil?
2711
+
2712
+ members = type.is_a?(Type::Union) ? type.members : [type]
2713
+ members.all? { |member| member.is_a?(Type::Bot) || member.is_a?(Type::Constant) }
2523
2714
  end
2524
2715
 
2525
2716
  # The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
@@ -2538,14 +2729,13 @@ module Rigor
2538
2729
  class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
2539
2730
  end
2540
2731
 
2541
- # Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
2542
- # receiver narrowing across stable receivers"). When the predicate's receiver is
2732
+ # Stable single-hop method-chain narrowing. When the predicate's receiver is
2543
2733
  # `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
2544
2734
  # narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
2545
2735
  # dominated body's identical chain reads then observe the narrowed type through
2546
2736
  # `ExpressionTyper#call_type_for`'s lookup.
2547
2737
  #
2548
- # Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
2738
+ # Heuristic-by-design: a second call to `x.last` could in
2549
2739
  # principle return a different value than the first. The chain is dropped on (1)
2550
2740
  # receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
2551
2741
  # any intervening call against the same root receiver (handled by
@@ -2864,9 +3054,7 @@ module Rigor
2864
3054
  # - For each fact, the corresponding positional argument (matched by parameter name in
2865
3055
  # the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
2866
3056
  # apply.
2867
- # - When the target is `self`, narrowing applies to the receiver — but the engine does
2868
- # not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
2869
- # but currently produce no scope edits.
3057
+ # - When the target is `self`, narrowing applies to the receiver ({#apply_self_fact}).
2870
3058
  def analyse_rbs_extended_contribution(node, scope)
2871
3059
  method_def = resolve_rbs_extended_method(node, scope)
2872
3060
  return nil if method_def.nil?
@@ -3403,8 +3591,8 @@ module Rigor
3403
3591
  # extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
3404
3592
  # written in a file outside the analysed set are both invisible to the walk behind
3405
3593
  # {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
3406
- # complete — RBS says nothing about a class it does not declare. A shape that turned this table into a positive
3407
- # `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3594
+ # complete — RBS says nothing about a class it does not declare. A shape that turned this table
3595
+ # into a positive `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3408
3596
  # `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
3409
3597
  # it, trading the unreachable-clause false positive for an undefined-method one — the same trap
3410
3598
  # #657 recorded on the `Constant` carrier.
@@ -3586,7 +3774,7 @@ module Rigor
3586
3774
  rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
3587
3775
  end
3588
3776
 
3589
- # The four receiver shapes with a scope binding the edges can rebind, mirroring
3777
+ # The three receiver shapes with a scope binding the edges can rebind, mirroring
3590
3778
  # `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
3591
3779
  # the caller declines.
3592
3780
  def receiver_binding_type(receiver, scope)
@@ -3696,7 +3884,7 @@ module Rigor
3696
3884
 
3697
3885
  # Walks a call chain down to its `&.` link, collecting the suffix method names applied
3698
3886
  # above it (outermost first). Declines unless the chain bottoms out in a `&.` call on a
3699
- # direct local / ivar read — those are the two bindings a scope can rewrite — and unless
3887
+ # direct local, instance-variable, global or constant read — the slots a scope can rewrite — and unless
3700
3888
  # every suffix is a bare reader or predicate, since an argument or a block could make the
3701
3889
  # answer depend on something other than the receiver.
3702
3890
  #
@@ -31,6 +31,12 @@ module Rigor
31
31
  # pure-value operand never reached, so a chained `b.push(2).size`, an argument `puts(b.push(2))` or
32
32
  # `d.map!.with_index { … }` left `b` / `d` on the literal their assignment wrote.
33
33
  #
34
+ # Issue #3163 — a `def` inside a block or lambda of the expression is looked into for one thing: an in-place
35
+ # mutation of an instance variable, class variable or global ({.method_mutation?}). The block may call the
36
+ # method it defines, and the evaluator floors such a variable after the block
37
+ # (`StatementEvaluator#floor_block_defined_method_mutations`), which it reaches only for an operand this module
38
+ # answers for: `puts([1].each { def h; $buf << 1; end; h })`.
39
+ #
34
40
  # Short-circuiting and allocation-free outside a mutator-named call: the evaluator asks it of every call's
35
41
  # operands, and asks it again of each operand it threads. A found effect stops the recursion without a
36
42
  # `return` out of the child block, which would allocate once per frame it unwinds and make a deep literal
@@ -58,12 +64,15 @@ module Rigor
58
64
  ].freeze
59
65
  JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
60
66
  SCOPE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
67
+ NON_LOCAL_READ_NODES = Set[
68
+ Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
69
+ ].freeze
61
70
  OPAQUE_NODES = Set[
62
71
  Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefinedNode
63
72
  ].freeze
64
73
  private_constant :LOCAL_WRITE_NODES, :OUTLIVING_WRITE_NODES, :INSTANCE_WRITE_NODES, :GLOBAL_WRITE_NODES,
65
74
  :COMPOUND_LOCAL_WRITES,
66
- :COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :OPAQUE_NODES
75
+ :COMPOUND_INSTANCE_WRITES, :JUMP_NODES, :SCOPE_NODES, :NON_LOCAL_READ_NODES, :OPAQUE_NODES
67
76
 
68
77
  module_function
69
78
 
@@ -79,7 +88,7 @@ module Rigor
79
88
  return true if OUTLIVING_WRITE_NODES.include?(klass)
80
89
  return true if jumps && JUMP_NODES.include?(klass)
81
90
  return true if klass == Prism::CallNode && outliving_mutation?(node, nesting)
82
- return false if OPAQUE_NODES.include?(klass)
91
+ return block_defined_method_mutation?(node, nesting) if OPAQUE_NODES.include?(klass)
83
92
 
84
93
  nesting += 1 if SCOPE_NODES.include?(klass)
85
94
  jumps &&= !JumpTargets.boundary?(node)
@@ -89,6 +98,38 @@ module Rigor
89
98
  end
90
99
  private_class_method :effect?
91
100
 
101
+ # Issue #3163 — the one effect an opaque node has: a `def` under a block or lambda whose body mutates a
102
+ # non-local in place ({.method_mutation?}).
103
+ def block_defined_method_mutation?(node, nesting)
104
+ node.instance_of?(Prism::DefNode) && nesting.positive? && method_mutation?(node)
105
+ end
106
+ private_class_method :block_defined_method_mutation?
107
+
108
+ # True when `node`, a `def` or anything in its body, holds a call or index write
109
+ # that mutates an instance variable, class variable or global in place: a {MutationWidening::SHAPE_MUTATORS}
110
+ # name, or an index `||=` / `&&=` / `op=`, on a read of one. A class or module body inside the method is not
111
+ # looked into; a `def` nested in it is.
112
+ def method_mutation?(node)
113
+ klass = node.class
114
+ if klass == Prism::CallNode
115
+ return true if MutationWidening::SHAPE_MUTATORS.include?(node.name) && non_local_read?(node.receiver)
116
+ elsif OUTLIVING_WRITE_NODES.include?(klass)
117
+ return true if node.respond_to?(:receiver) && non_local_read?(node.receiver)
118
+ elsif klass != Prism::DefNode && OPAQUE_NODES.include?(klass)
119
+ return false
120
+ end
121
+
122
+ found = false
123
+ node.rigor_each_child { |child| found ||= method_mutation?(child) }
124
+ found
125
+ end
126
+ private_class_method :method_mutation?
127
+
128
+ def non_local_read?(node)
129
+ NON_LOCAL_READ_NODES.include?(node.class)
130
+ end
131
+ private_class_method :non_local_read?
132
+
92
133
  # True when `node` is a call the straight-line widening would answer for a variable that outlives the
93
134
  # expression: a {MutationWidening::SHAPE_MUTATORS} name on a receiver whose {ReceiverAlias.mutated_reads},
94
135
  # or the local an element read is rooted at ({ElementReadWidening.element_read_path}, `a[0] << e`), reach