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
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../reflection"
6
+ require_relative "../../source/node_children"
7
+ require_relative "../../type"
8
+
9
+ module Rigor
10
+ module Analysis
11
+ module CheckRules
12
+ # Issue #3036 — answers, for a call node `flow.always-raises` proved raises, whether an enclosing
13
+ # `begin` (or `def … rescue … end`, which Prism wraps in the same `BeginNode`) catches the raise
14
+ # before it escapes. When a rescue clause may catch the raised class the exception flows to the
15
+ # handler, so the rule stays silent.
16
+ #
17
+ # The check is deliberately conservative in BOTH directions:
18
+ #
19
+ # - it suppresses only when a clause NAMES a class the raised class orders at-or-below
20
+ # (`rescue ZeroDivisionError`, `rescue StandardError`, `rescue Exception`, or a bare `rescue`,
21
+ # which is `StandardError`), resolved through the typer so lexical constant lookup applies. An
22
+ # exception reference that is not a constant (`rescue *ERRORS`, `rescue klass`) contributes no
23
+ # coverage, and an unresolvable / `:unknown` ordering keeps the diagnostic — a wrong silence is
24
+ # worse than a kept finding.
25
+ # - it never crosses a body boundary ({BODY_BOUNDARIES}): a `def`, `->`, or block body runs when
26
+ # called, not while the `begin` body evaluates, and `BEGIN` / `END` blocks run on their own
27
+ # schedule, so an outer rescue cannot be proven to see their raise. Class / module bodies are
28
+ # NOT boundaries — they evaluate inline, so their raises DO reach the enclosing rescue.
29
+ #
30
+ # Prism nodes carry no parent pointers, so ancestry is answered from a `node => parent` table
31
+ # built lazily on the first query: a file with no zero-division candidate never pays for it.
32
+ class AlwaysRaisesShield
33
+ # The exception class the rule proves today — Integer `/# % div modulo divmod` by a literal
34
+ # zero divisor. A rescue catches it when the clause names the class itself or an ancestor.
35
+ RAISED_CLASS = "ZeroDivisionError"
36
+
37
+ # The implicit class of a bare `rescue` / `rescue => e`.
38
+ BARE_RESCUE_CLASS = "StandardError"
39
+
40
+ # Node classes whose bodies do not execute inside the enclosing `begin` — see the class
41
+ # comment. Mirrors the deferred-body set `Inference::ScopeIndexer` already tracks.
42
+ BODY_BOUNDARIES = [
43
+ Prism::DefNode, Prism::LambdaNode, Prism::BlockNode,
44
+ Prism::PreExecutionNode, Prism::PostExecutionNode
45
+ ].freeze
46
+
47
+ def initialize(root)
48
+ @root = root
49
+ @parent_map = nil
50
+ end
51
+
52
+ # True when `node` sits in the protected body of a `begin` whose rescue chain may catch
53
+ # {RAISED_CLASS}. `scope` is the scope recorded for `node`; it resolves the clause's exception
54
+ # constants lexically. A nil scope or a node outside `root`'s subtree answers false.
55
+ def rescued?(node, scope)
56
+ return false if scope.nil?
57
+
58
+ parents = parent_map
59
+ child = node
60
+ current = parents[node]
61
+ while current
62
+ return true if protected_body?(current, child, scope)
63
+ return false if BODY_BOUNDARIES.any? { |klass| current.is_a?(klass) }
64
+
65
+ child = current
66
+ current = parents[current]
67
+ end
68
+ false
69
+ end
70
+
71
+ private
72
+
73
+ # True when `node` is a `begin` whose protected body (`statements`) contains `child` — the
74
+ # ascent arrived through the body rather than through a rescue / else / ensure clause, whose
75
+ # raises this `begin` does not catch — and whose rescue chain may catch {RAISED_CLASS}. When
76
+ # the chain does NOT catch, the ascent continues: an outer `begin` may still.
77
+ def protected_body?(node, child, scope)
78
+ return false unless node.is_a?(Prism::BeginNode)
79
+ return false unless child.equal?(node.statements)
80
+ return false if node.rescue_clause.nil?
81
+
82
+ rescue_chain_catches?(node.rescue_clause, scope)
83
+ end
84
+
85
+ # Any clause of the `rescue … rescue …` chain may catch — clauses are tried in order and the
86
+ # first match wins, so one catching clause is enough.
87
+ def rescue_chain_catches?(clause, scope)
88
+ while clause
89
+ return true if rescue_clause_catches?(clause, scope)
90
+
91
+ clause = clause.subsequent
92
+ end
93
+ false
94
+ end
95
+
96
+ # One `rescue` clause catches {RAISED_CLASS} when any class it names orders at-or-above the
97
+ # raised class. A bare `rescue` names {BARE_RESCUE_CLASS}.
98
+ def rescue_clause_catches?(clause, scope)
99
+ exceptions = clause.exceptions
100
+ return class_catches?(BARE_RESCUE_CLASS, scope) if exceptions.empty?
101
+
102
+ exceptions.any? do |exception|
103
+ class_name = exception_class_name(exception, scope)
104
+ class_name && class_catches?(class_name, scope)
105
+ end
106
+ end
107
+
108
+ # The class a rescue exception reference names, or nil when it is not a constant path (a splat,
109
+ # a dynamic expression) or does not resolve to a class object. Only `Type::Singleton` counts:
110
+ # the rescue operand is the class itself, and an instance-typed constant cannot be proven to
111
+ # match.
112
+ def exception_class_name(node, scope)
113
+ return nil unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
114
+
115
+ type = scope.type_of(node)
116
+ type.class_name if type.is_a?(Type::Singleton)
117
+ end
118
+
119
+ # `class_ordering(raised, rescued)`: `:equal` / `:subclass` mean the rescued class IS the raised
120
+ # class or an ancestor of it — the clause catches. `:superclass`, `:disjoint`, and `:unknown`
121
+ # all keep the diagnostic.
122
+ def class_catches?(rescued_class, scope)
123
+ %i[equal subclass].include?(
124
+ Rigor::Reflection.class_ordering(RAISED_CLASS, rescued_class, scope: scope)
125
+ )
126
+ end
127
+
128
+ # `node => parent` for every descendant of `root`, built on first use. Iterative so a deeply
129
+ # nested file cannot overflow the stack the way a recursive walk could.
130
+ def parent_map
131
+ @parent_map ||= begin
132
+ parents = {}
133
+ stack = [@root]
134
+ until stack.empty?
135
+ node = stack.pop
136
+ node.rigor_each_child do |child|
137
+ parents[child] = node
138
+ stack << child
139
+ end
140
+ end
141
+ parents
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -23,10 +23,11 @@ module Rigor
23
23
  # `BlockNode` ancestors. Mutation tracking through loop bodies is incomplete (`shift = 0; loop {
24
24
  # shift += 7 }` keeps `shift` at `Constant<7>` per the analyzer), so a `Constant<bool>` predicate
25
25
  # inside the loop body is suspect.
26
- # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?`. These
27
- # read like the user explicitly checking for a runtime condition that the type system already proves
28
- # can't happen — Rigor's strict-on-returns RBS often disagrees with the user's defensive check (e.g.
29
- # `Module#name` returns `String` per RBS but anonymous classes really do return `nil`). Skipping
26
+ # - **Defensive predicate calls**: `.nil?` / `.empty?` / `.zero?` / `.any?` / `.none?` / `.all?` /
27
+ # `.respond_to?`. These read like the user explicitly checking for a runtime condition that the type
28
+ # system already proves can't happen — Rigor's strict-on-returns RBS often disagrees with the user's
29
+ # defensive check (e.g. `Module#name` returns `String` per RBS but anonymous classes really do return
30
+ # `nil`). Skipping
30
31
  # these forms keeps the rule useful for genuine logic errors without faulting defensive code.
31
32
  #
32
33
  # Also skipped (so the literal-only `unreachable-branch` rule doesn't double-fire alongside this one):
@@ -53,8 +54,6 @@ module Rigor
53
54
  NODE_CLASSES = [Prism::IfNode, Prism::UnlessNode].freeze
54
55
  RULE_WALK_GATES = [:loop_or_block].freeze
55
56
 
56
- # @return one entry per qualifying predicate. Empty when the tree carries no firing
57
- # predicates.
58
57
  def initialize(scope_index)
59
58
  @scope_index = scope_index
60
59
  @results = []
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ module CheckRules
6
+ # Issue #2786 — what the per-call-node rules ({CheckRules.call_node_diagnostics}) each derived on their own:
7
+ # the call node's index scope, the type of its receiver under that scope, and the RBS signature the call
8
+ # resolves to on that receiver. One object per call node is built before the rules run and handed to every
9
+ # rule that reads them.
10
+ #
11
+ # The receiver type is computed lazily, on the first rule that asks, from the same `(scope, node)` pair each
12
+ # rule typed before: `scope_index[call_node]` and `call_node.receiver`. Every later rule reads that value
13
+ # instead of re-typing the receiver chain. This is not a memo over `type_of`: nothing is served across call
14
+ # nodes or across scopes, and no fact is computed anywhere but at the first point a rule computed it before.
15
+ # It rests on the `Scope#type_of` contract that one `(scope, node)` pair types structurally equal within a run
16
+ # *under the same ambient inputs* (`docs/internal-spec/inference-engine.md`), which holds here because of
17
+ # an invariant of the rule list: between the first rule that types the receiver and the last that reads it, no
18
+ # frame is installed and nothing types a node inside the receiver's subtree under another state or adds an
19
+ # `optimistic_origins` mark there. The other typings in that window are the call's arguments (outside the
20
+ # subtree), `receiver.receiver` under the same scope (which rewrites what the receiver's typing already
21
+ # wrote), and the key-presence re-walk, which runs on `with_isolated_side_tables`. A rule inserted between
22
+ # two readers that breaks this must type the receiver itself.
23
+ #
24
+ # Of the typing, only the receiver is shared. A rule that types a sub-expression (`receiver.receiver`), an
25
+ # argument, or any node under a scope other than the call's keeps its own `type_of` call: arguments are typed
26
+ # from their own index entry by `call.argument-type-mismatch` and from the call's by `flow.always-raises`, and
27
+ # the two meet only on a divisor.
28
+ class CallFacts
29
+ attr_reader :call_node, :scope_index, :scope
30
+
31
+ def initialize(call_node, scope_index)
32
+ @call_node = call_node
33
+ @scope_index = scope_index
34
+ @scope = scope_index[call_node]
35
+ @receiver_type = nil
36
+ @receiver_signature = nil
37
+ @receiver_signature_resolved = false
38
+ end
39
+
40
+ # `scope.type_of(call_node.receiver)`, computed once. Callers check that the call has a receiver and that
41
+ # {#scope} is not nil first, as they did before sharing it.
42
+ def receiver_type
43
+ @receiver_type ||= @scope.type_of(@call_node.receiver)
44
+ end
45
+
46
+ # The signature `CheckRules.receiver_signature` resolves, its block run on the first ask only. A nil answer
47
+ # is remembered too. A block that raises leaves it unresolved, so the next ask raises as it did before.
48
+ def receiver_signature
49
+ return @receiver_signature if @receiver_signature_resolved
50
+
51
+ signature = yield
52
+ @receiver_signature_resolved = true
53
+ @receiver_signature = signature
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -35,8 +35,8 @@ module Rigor
35
35
  # exactly like the legacy walk.
36
36
  NODE_CLASSES = [Prism::DefNode].freeze
37
37
 
38
- # Returns `Array<{def_class:, def_name:, write_node:}>` — one entry per dead-assignment write. Empty
39
- # when the tree has no qualifying writes.
38
+ # One entry per dead-assignment write, as `{def_node:, write_node:}`. Empty when the tree has no
39
+ # qualifying writes.
40
40
  def initialize(scope_index)
41
41
  @scope_index = scope_index
42
42
  @results = []
@@ -24,10 +24,10 @@ module Rigor
24
24
  # currently declaration-sourced ivar*, so a second hop (`c = @count; d = c`) and an `||=` rewrite land on
25
25
  # the plain `with_local` path and carry no mark.
26
26
  #
27
- # A branch join is NOT one of those cases, contrary to what this comment claimed when it landed: both
28
- # arms of `if c then r = @count else r = @count end` stamp `(:local, :r)`, and `join_declaration_sourced`
29
- # INTERSECTS, so the mark survives. Only an *asymmetric* join drops it, and by that intersection rather
30
- # than by reaching `with_local`. The normative statement — establishment, drop, join and the full list of
27
+ # A branch join keeps the mark when both arms stamp it: `if c then r = @count else r = @count end` stamps
28
+ # `(:local, :r)` on each arm, and `join_declaration_sourced` INTERSECTS, so the mark survives. Only an
29
+ # *asymmetric* join drops it, by that intersection rather than by reaching `with_local`. The normative
30
+ # statement — establishment, drop, join and the full list of
31
31
  # unmarked shapes, each verified against the implementation — is
32
32
  # `docs/internal-spec/inference-engine.md` § "Declaration-sourced provenance mark (ADR-58)"; keep this
33
33
  # comment subordinate to it.
@@ -59,6 +59,14 @@ module Rigor
59
59
  end
60
60
  end
61
61
 
62
+ # Issue #2656 — true when `node` reads a local a class guard narrowed from a `Dynamic` value
63
+ # ({Scope#with_gradual_narrowing_mark}). `call.argument-type-mismatch` judges such an argument by its class
64
+ # rather than against the parameter's literals (`CheckRules#gradual_narrowing_accepted?`); the other rules
65
+ # read the narrowed `Nominal` as before.
66
+ def gradual_narrowed?(node, scope)
67
+ node.is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(node.name)
68
+ end
69
+
62
70
  # The globals whose declared seeds `node` still reads, empty for none: a read of a marked global, or of a
63
71
  # local copied from one or more (`sep = $/`, `Scope#declaration_sourced_global_copies`), bare or in
64
72
  # parentheses (`($stdout)`).
@@ -36,8 +36,8 @@ module Rigor
36
36
  # a Hash literal, loops and blocks included.
37
37
  NODE_CLASSES = [Prism::HashNode, Prism::KeywordHashNode].freeze
38
38
 
39
- # Returns `Array<{hash_node:, key_node:, first_key_node:, key_label:}>` — one entry per LATER
40
- # occurrence of a repeated literal key (`key_node` is the repeat the diagnostic points at;
39
+ # One entry per LATER occurrence of a repeated literal key, carrying `hash_node`, `key_node`,
40
+ # `first_key_node` and `key_label` (`key_node` is the repeat the diagnostic points at;
41
41
  # `first_key_node` is the earliest occurrence its message references). Empty when the tree has no
42
42
  # qualifying duplicates.
43
43
  def initialize(scope_index)
@@ -29,6 +29,13 @@ module Rigor
29
29
  # under the ivar's `@`-prefixed name, which no local can spell — because the write and the read are
30
30
  # separate statements. The mark is method-local: the class-ivar census that seeds a sibling method's
31
31
  # ivars types the write's RHS without the inference seed, so a cross-method read is not a lower bound.
32
+ # - `Array(param)` (issue #2280): the conversion passes the parameter's elements through, so
33
+ # `Array(param).first.upcase` reads the lower bound back.
34
+ # - an `if` / `unless` / ternary whose value is the parameter (issue #2280): `(c ? param : param).upcase`.
35
+ # Unlike `&&` / `||`, a branch that does not hold the parameter keeps its diagnostic: the form is rooted
36
+ # when every branch that can run is. A predicate the engine folds to one arm (the same certainty
37
+ # `StatementEvaluator#eval_if` elides the dead arm on) leaves only that arm to decide, so
38
+ # `(c ? param : 1).upcase` declines when `c` folds truthy and reports `Integer` when it folds falsey.
32
39
  #
33
40
  # Every value derived from an inferred parameter by dispatch is itself a lower bound, so firing against it
34
41
  # is an FP by the same construction. No-op on a normal `check` run — `Scope#inferred_param?` is false
@@ -40,40 +47,105 @@ module Rigor
40
47
  # length, which is tiny in practice).
41
48
  MAX_DEPTH = 64
42
49
 
43
- def rooted?(node, scope, depth = 0)
50
+ # `fold` is whether `scope` is still the scope the walked node is evaluated in. The callers pass the scope
51
+ # at the start of the expression; once the walk passes something evaluated earlier inside it (a leading
52
+ # statement, the left operand of `&&`, an earlier literal element, a conditional's predicate), that
53
+ # earlier code may have rebound what a nested predicate reads (`(c = false; c ? param : 1)`), so a
54
+ # conditional below no longer folds its predicate in `scope`.
55
+ def rooted?(node, scope, depth = 0, fold: true)
44
56
  return false if node.nil? || depth > MAX_DEPTH
45
57
 
46
58
  case node
47
59
  when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode then scope.inferred_param?(node.name)
48
- when Prism::CallNode then rooted?(node.receiver, scope, depth + 1)
60
+ when Prism::CallNode then call_rooted?(node, scope, depth + 1, fold)
49
61
  when Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode
50
- literal_rooted?(node, scope, depth + 1)
51
- else combining_rooted?(node, scope, depth + 1)
62
+ literal_rooted?(node, scope, depth + 1, fold)
63
+ else combining_rooted?(node, scope, depth + 1, fold)
64
+ end
65
+ end
66
+
67
+ def call_rooted?(node, scope, depth, fold)
68
+ return rooted?(node.receiver, scope, depth, fold: fold) unless array_conversion?(node, scope)
69
+
70
+ rooted?(node.arguments.arguments.first, scope, depth, fold: fold)
71
+ end
72
+
73
+ # A receiverless `Array(x)` reaching Kernel#Array: exactly one plain argument, no block, and no project
74
+ # `Array` on `self`'s class or its project ancestors to bind ahead of Kernel's.
75
+ def array_conversion?(node, scope)
76
+ node.receiver.nil? && node.name == :Array && node.block.nil? &&
77
+ node.arguments&.arguments&.size == 1 && !project_array_method?(scope)
78
+ end
79
+
80
+ def project_array_method?(scope)
81
+ self_type = scope.self_type
82
+ case self_type
83
+ when Type::Nominal then scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :instance)
84
+ when Type::Singleton
85
+ scope.discovered_method_through_ancestors?(self_type.class_name, :Array, :singleton)
86
+ else false
52
87
  end
53
88
  end
54
89
 
55
90
  # An array or hash literal is rooted when any element is: the engine reads the element back precisely.
56
- def literal_rooted?(node, scope, depth)
91
+ def literal_rooted?(node, scope, depth, fold)
57
92
  parts = node.is_a?(Prism::AssocNode) ? [node.key, node.value] : node.elements
58
- parts.any? { |part| rooted?(part, scope, depth) }
93
+ parts.each_with_index.any? { |part, index| rooted?(part, scope, depth, fold: fold && index.zero?) }
59
94
  end
60
95
 
61
96
  # Value-combining forms: the produced value is a union of the operands, so it is a lower bound when ANY
62
97
  # operand is inferred-parameter-rooted (`cps[i] - x rescue -1`, `a && param.foo`, `param || b`). Declining
63
98
  # on the whole expression is FP-safe by the same lower-bound argument.
64
- def combining_rooted?(node, scope, depth)
99
+ def combining_rooted?(node, scope, depth, fold)
65
100
  case node
66
101
  when Prism::RescueModifierNode
67
- rooted?(node.expression, scope, depth) || rooted?(node.rescue_expression, scope, depth)
68
- when Prism::AndNode, Prism::OrNode then rooted?(node.left, scope, depth) || rooted?(node.right, scope, depth)
69
- when Prism::ParenthesesNode then rooted?(last_statement(node.body), scope, depth)
70
- when Prism::StatementsNode then rooted?(node.body.last, scope, depth)
102
+ rooted?(node.expression, scope, depth, fold: fold) ||
103
+ rooted?(node.rescue_expression, scope, depth, fold: false)
104
+ when Prism::AndNode, Prism::OrNode
105
+ rooted?(node.left, scope, depth, fold: fold) || rooted?(node.right, scope, depth, fold: false)
106
+ when Prism::ParenthesesNode then body_rooted?(node.body, scope, depth, fold)
107
+ when Prism::StatementsNode then body_rooted?(node, scope, depth, fold)
108
+ when Prism::IfNode, Prism::UnlessNode then conditional_rooted?(node, scope, depth, fold)
109
+ when Prism::ElseNode then rooted?(node.statements, scope, depth, fold: fold)
110
+ else false
111
+ end
112
+ end
113
+
114
+ # A body's value is its last statement; any statement before it runs first.
115
+ def body_rooted?(body, scope, depth, fold)
116
+ return rooted?(body, scope, depth, fold: fold) unless body.is_a?(Prism::StatementsNode)
117
+
118
+ rooted?(body.body.last, scope, depth, fold: fold && body.body.size == 1)
119
+ end
120
+
121
+ # Every branch that can run must be rooted; an absent branch is `nil`, never rooted. The predicate is
122
+ # typed only when the branches disagree, so a conditional that never mentions a parameter costs nothing.
123
+ # An arm runs after the predicate, so a conditional nested in one never folds.
124
+ def conditional_rooted?(node, scope, depth, fold)
125
+ truthy_rooted, falsey_rooted = arms(node).map { |arm| rooted?(arm, scope, depth, fold: false) }
126
+ return truthy_rooted if truthy_rooted == falsey_rooted
127
+ return false unless fold
128
+
129
+ case predicate_certainty(node.predicate, scope)
130
+ when :truthy then truthy_rooted
131
+ when :falsey then falsey_rooted
71
132
  else false
72
133
  end
73
134
  end
74
135
 
75
- def last_statement(body)
76
- body.is_a?(Prism::StatementsNode) ? body.body.last : body
136
+ # `[runs when the predicate is truthy, runs when it is falsey]`.
137
+ def arms(node)
138
+ node.is_a?(Prism::IfNode) ? [node.statements, node.subsequent] : [node.else_clause, node.statements]
139
+ end
140
+
141
+ # The arm certainty `StatementEvaluator#branch_certainty` elides on: a decidable version guard, else the
142
+ # predicate's folded truthiness unless an optimistic carrier props it up.
143
+ def predicate_certainty(predicate, scope)
144
+ guard = Inference::VersionGuard.verdict(predicate)
145
+ return guard if guard
146
+ return nil unless Inference::OptimisticOrigin.resolve(predicate, scope).nil?
147
+
148
+ Inference::Narrowing.predicate_certainty(scope.type_of(predicate))
77
149
  end
78
150
  end
79
151
  end
@@ -6,6 +6,7 @@ require_relative "rule_walk"
6
6
  require_relative "../../scope/resolution_chain"
7
7
  require_relative "../../source/constant_path"
8
8
  require_relative "../../source/node_children"
9
+ require_relative "../../inference/census_foreign_block"
9
10
 
10
11
  module Rigor
11
12
  module Analysis
@@ -28,6 +29,9 @@ module Rigor
28
29
  # doesn't yet model.
29
30
  # - Nested classes / modules / defs inside a method body are barriers, mirroring the indexer's
30
31
  # `IVAR_BARRIER_NODES` policy.
32
+ # - Issue #2943: a write directly in a block or `class << obj` body whose `self` is provably not an instance
33
+ # of the class (`Other.new.instance_eval { @r = 1 }`), by the class-ivar census's rule
34
+ # ({Inference::CensusForeignBlock}).
31
35
  class IvarWriteCollector
32
36
  BARRIER_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
33
37
  private_constant :BARRIER_NODES
@@ -44,8 +48,9 @@ module Rigor
44
48
  NODE_CLASSES = [Prism::DefNode, Prism::CallNode].freeze
45
49
  RULE_WALK_GATES = %i[inside_def detached_ivar_facet].freeze
46
50
 
47
- # Returns `Hash[class_name (String) => Hash[ivar_name (Symbol) => Array<{node:, type:, declared:,
48
- # method_name:, visibility:, per_invocation:, action:}>]]`. Empty when the tree has no qualifying writes.
51
+ # Groups the writes by class qualified name, then by ivar name. Each write carries `node`, `type`,
52
+ # `declared`, `method_name`, `visibility`, `per_invocation` and `action`. Empty when the tree has no
53
+ # qualifying writes.
49
54
  # `declared` is the translated type of the class's RBS declaration of the ivar — an `@x: T` member or
50
55
  # the slot an `attr_*` member implies, own or inherited — and nil when the RBS declares none.
51
56
  #
@@ -155,7 +160,8 @@ module Rigor
155
160
 
156
161
  def collect_def_writes(def_node, qualified_prefix)
157
162
  return if def_node.body.nil? || qualified_prefix.empty?
158
- return if def_node.receiver.is_a?(Prism::SelfNode)
163
+ # A def with a receiver writes the ivars of another object than an instance (#3155).
164
+ return if def_node.receiver
159
165
 
160
166
  class_name = qualified_prefix.join("::")
161
167
  gather_writes(def_node.body, class_name, def_node.name)
@@ -167,7 +173,13 @@ module Rigor
167
173
  record_write(node, class_name, method_name) if node.is_a?(Prism::InstanceVariableWriteNode)
168
174
  return if BARRIER_NODES.any? { |klass| node.is_a?(klass) }
169
175
 
170
- node.rigor_each_child { |child| gather_writes(child, class_name, method_name) }
176
+ # #2943 — a block or `class <<` body whose `self` is provably another object writes that object's ivars, as
177
+ # the class-ivar census reads it ({Inference::CensusForeignBlock}); a `define_method` block nested in it that
178
+ # runs on an instance again still counts. A `def` it reenters stays a barrier here.
179
+ children = Inference::CensusForeignBlock.census_children(node, class_name)
180
+ return node.rigor_each_child { |child| gather_writes(child, class_name, method_name) } unless children
181
+
182
+ children.each { |child| gather_writes(child, class_name, method_name) }
171
183
  end
172
184
 
173
185
  def record_write(node, class_name, method_name)
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../../inference/scope_indexer"
6
6
  require_relative "../../source/node_children"
7
7
  require_relative "rebinding_block_callees"
8
+ require_relative "replaced_singleton_defs"
8
9
 
9
10
  module Rigor
10
11
  module Analysis
@@ -42,6 +43,11 @@ module Rigor
42
43
 
43
44
  def refinements = (@refinements ||= Inference::InEffectRefinements.new(@root))
44
45
 
46
+ # Issue #3155 — the file's singleton defs a later certain def replaces, for `def.return-type-mismatch`.
47
+ def replaced_singleton_defs(scope_index)
48
+ @replaced_singleton_defs ||= ReplacedSingletonDefs.new(@root, scope_index)
49
+ end
50
+
45
51
  # Issue #1817 — the file's block-passing calls, for `call.unresolved-toplevel`'s rebinding-callee decline.
46
52
  def block_calls = (@block_calls ||= RebindingBlockCallees::Sites.new(@root))
47
53
 
@@ -152,7 +158,6 @@ module Rigor
152
158
  end
153
159
  end
154
160
 
155
- # `o.singleton_class`.
156
161
  def singleton_class_of_local?(node)
157
162
  node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? &&
158
163
  node.receiver.is_a?(Prism::LocalVariableReadNode)
@@ -12,16 +12,13 @@ module Rigor
12
12
  # decides, with NO loop / block suppression and NO threaded context — so the collector declares no
13
13
  # `RULE_WALK_GATES` and ignores the walk's `context`.
14
14
  #
15
- # The per-node dispatch itself stays in `CheckRules` (the verbatim `case` from `diagnose`'s former inline
16
- # walk — only the traversal moved, ADR-53 WD4) and is handed in as a callable built in the `CheckRules`
17
- # module context, so its calls to the private diagnostic builders remain implicit-self. The collector
18
- # only owns the traversal hook and accumulation. Unlike the fact collectors its `#results` are the
19
- # accumulated {Diagnostic}s, in the same emission order the inline `NodeWalker.each` produced them (the
20
- # shared walk is the same visit-before-descend DFS over `compact_child_nodes`).
15
+ # The per-node dispatch stays in `CheckRules` (ADR-53 WD4) and is handed in as a callable built in the
16
+ # `CheckRules` module context, so its calls to the private diagnostic builders remain implicit-self. The
17
+ # collector only owns the traversal hook and accumulation; `#results` are the accumulated {Diagnostic}s in
18
+ # the walk's emission order.
21
19
  class MainPassCollector
22
20
  # The node classes the main pass branches on. A plain `Prism::IfNode` covers ternaries and postfix `if`
23
- # too (the legacy `when Prism::IfNode, Prism::UnlessNode` arm did the same). The three global-write
24
- # classes carry issue #1367's `global.*` rules.
21
+ # too. The global-write classes and `MultiWriteNode` carry issue #1367's `global.*` rules.
25
22
  NODE_CLASSES = [
26
23
  Prism::CallNode, Prism::DefNode, Prism::IfNode, Prism::UnlessNode,
27
24
  Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode