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,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../configuration/severity_profile"
6
+ require_relative "../../reflection"
7
+ require_relative "../../source/node_walker"
8
+ require_relative "../../scope/resolution_chain"
9
+ require_relative "../../type"
10
+ require_relative "../diagnostic"
11
+ require_relative "rule_ids"
12
+
13
+ module Rigor
14
+ module Analysis
15
+ module CheckRules
16
+ # ADR-128 WD6 / ADR-130 WD3 — `plugin.api-surface`. A call in plugin source on a value typed by a
17
+ # `Rigor::Plugin` interface (`Dynamic[Rigor::Plugin::_Scope]`, ...) to a member that interface does not
18
+ # declare. The method exists on the backing engine class, so the call works today; it is outside what Rigor
19
+ # promises to keep, so `call.undefined-method` would be wrong and this rule says that instead.
20
+ #
21
+ # Authored `:warning`, resolved `:off` by every profile and promoted only by the `plugin-api-surface`
22
+ # bleeding-edge feature (ADR-50 Decision 3, WD2), as `static.value-use.void` is.
23
+ #
24
+ # It fires only where all of these hold:
25
+ #
26
+ # - the call has an explicit receiver, typed `Dynamic[<Rigor::Plugin interface>]`, or that plus `nil` (the
27
+ # facet survives only where ADR-130 WD1 keeps it; a value that lost it at a join reads `Dynamic[top]`);
28
+ # - the interface declares no member of the name, and `Object` (with `Kernel` and `BasicObject`) declares
29
+ # none either: `nil?`, `respond_to?` and `is_a?` are every object's surface, not the engine's;
30
+ # - the call sits lexically inside a class whose discovered superclass chain reaches `Rigor::Plugin::Base`.
31
+ # `Base` itself is not such a class, so its own hooks, which hand interface values to engine helpers
32
+ # (`environment.class_ordering`), are not judged: they are engine code.
33
+ #
34
+ # The plugin-source test reads the class declarations, not a path: a third-party plugin gem keeps its plugin
35
+ # under `lib/` as Rigor's own engine does.
36
+ #
37
+ # The rule runs only when its resolved severity is not `:off` ({.active?}): the runner then adds its
38
+ # {Collector} to the shared {RuleWalk}. A default run builds no collector, so it types no extra receiver and
39
+ # files no ADR-46 dependency for the rule.
40
+ module PluginApiSurface
41
+ PLUGIN_BASE = "Rigor::Plugin::Base"
42
+
43
+ # One call the rule reports: the call and the first interface its receiver carries.
44
+ Result = Data.define(:call_node, :interface)
45
+
46
+ # The {RuleWalk} collector: one {Result} per reported call, in walk order.
47
+ class Collector
48
+ NODE_CLASSES = [Prism::CallNode].freeze
49
+
50
+ def initialize(scope_index)
51
+ @scope_index = scope_index
52
+ @results = []
53
+ end
54
+
55
+ # The legacy single-collector walk, the oracle the ADR-53 equivalence harness compares {RuleWalk} with.
56
+ def collect(root)
57
+ Source::NodeWalker.each(root) { |node| visit(node) if node.is_a?(Prism::CallNode) }
58
+ @results.freeze
59
+ end
60
+
61
+ def visit(node, _context = nil)
62
+ interface = PluginApiSurface.outside_interface(node, @scope_index[node])
63
+ @results << Result.new(call_node: node, interface: interface) if interface
64
+ end
65
+
66
+ def results
67
+ @results.freeze
68
+ end
69
+ end
70
+
71
+ module_function
72
+
73
+ # Whether the rule can report in a run under `configuration`: it is not in `disable:`, and its resolved
74
+ # severity is not `:off`. Default profiles resolve it `:off`; the `plugin-api-surface` bleeding-edge feature
75
+ # or an explicit `severity_overrides:` entry for the id turns it on.
76
+ def active?(configuration)
77
+ disabled = configuration.disabled_rules
78
+ return false if disabled.include?(RULE_PLUGIN_API_SURFACE) || disabled.include?("all")
79
+
80
+ Configuration::SeverityProfile.resolve(
81
+ rule: RULE_PLUGIN_API_SURFACE, authored_severity: :warning,
82
+ profile: configuration.severity_profile, overrides: configuration.severity_overrides,
83
+ bleeding_edge_overrides: configuration.bleeding_edge_severity_overrides
84
+ ) != :off
85
+ end
86
+
87
+ # The interface the call's receiver carries when the call is outside it, or nil.
88
+ def outside_interface(call_node, scope)
89
+ return nil if call_node.receiver.nil? || scope.nil?
90
+
91
+ interfaces = receiver_interfaces(scope.type_of(call_node.receiver))
92
+ return nil if interfaces.nil?
93
+
94
+ environment = scope.environment
95
+ method_name = call_node.name
96
+ return nil if interfaces.any? { |interface| declares?(environment, interface, method_name) }
97
+ return nil if object_member?(environment, method_name)
98
+ return nil unless plugin_source?(scope)
99
+
100
+ interfaces.first
101
+ end
102
+
103
+ def diagnostics(path, results)
104
+ results.map { |result| build(path, result.call_node, result.interface, result.call_node.name) }
105
+ end
106
+
107
+ # The interface facets the receiver carries, or nil when it is anything but `Dynamic[Interface]` arms and
108
+ # `nil`. A union whose non-nil arms include another type has a member the call may resolve on.
109
+ def receiver_interfaces(type)
110
+ case type
111
+ when Type::Dynamic
112
+ facet = type.static_facet
113
+ [facet] if facet.is_a?(Type::Interface)
114
+ when Type::Union
115
+ union_interfaces(type)
116
+ end
117
+ end
118
+
119
+ def union_interfaces(union)
120
+ interfaces = []
121
+ union.members.each do |member|
122
+ next if nil_type?(member)
123
+ return nil unless member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Interface)
124
+
125
+ interfaces << member.static_facet
126
+ end
127
+ interfaces.empty? ? nil : interfaces
128
+ end
129
+
130
+ def nil_type?(type)
131
+ (type.is_a?(Type::Constant) && type.value.nil?) ||
132
+ (type.is_a?(Type::Nominal) && type.class_name == "NilClass")
133
+ end
134
+
135
+ # An interface that does not resolve declares nothing the rule can read, so it is treated as declaring
136
+ # every member. A guard: the carrier exists only for an interface the loader translated, so no fixture
137
+ # reaches it.
138
+ def declares?(environment, interface, method_name)
139
+ definition = environment.rbs_loader.interface_definition(interface.name)
140
+ return true if definition.nil?
141
+
142
+ definition.methods.key?(method_name.to_sym)
143
+ end
144
+
145
+ def object_member?(environment, method_name)
146
+ !Rigor::Reflection.instance_method_definition("Object", method_name, environment: environment).nil?
147
+ end
148
+
149
+ # Whether the call is plugin source: lexically inside a class that inherits `Rigor::Plugin::Base`. A
150
+ # `Rigor::Plugin::Base` body is not: its chain holds `Base` only as itself. An unrecorded nesting (a scope
151
+ # built outside a declaration walk) is not either.
152
+ def plugin_source?(scope)
153
+ nesting = scope.lexical_nesting
154
+ return false if nesting.nil? || nesting.empty?
155
+
156
+ nesting.any? { |name| inherits_plugin_base?(scope, name) }
157
+ end
158
+
159
+ # Whether `class_name`'s instance chain ({Scope::ResolutionChain}, the one ancestry reader) holds
160
+ # `Rigor::Plugin::Base` below the class itself. Reading it files the ADR-46 class dependencies, so
161
+ # an incremental recheck re-judges the call when a declaration on the chain changes in another file. A
162
+ # truncated chain reads as no plugin: the rule withholds where it cannot see the whole chain.
163
+ def inherits_plugin_base?(scope, class_name)
164
+ root = class_name.to_s
165
+ chain = Scope::ResolutionChain.for(scope, root, :instance, :methods)
166
+ chain.record(scope)
167
+ return false if chain.truncated?
168
+
169
+ chain.entries.any? { |entry| entry.name != root && plugin_base_entry?(entry) }
170
+ end
171
+
172
+ # `Rigor::Plugin::Base` is external where only its RBS and the gem declare it, and a project entry where the
173
+ # analysed tree holds its source (Rigor's own `lib`, or a reopen).
174
+ def plugin_base_entry?(entry)
175
+ entry.external? ? entry.candidates.include?(PLUGIN_BASE) : entry.name == PLUGIN_BASE
176
+ end
177
+
178
+ def build(path, call_node, interface, method_name)
179
+ Diagnostic.from_message_loc(
180
+ call_node,
181
+ rule: RULE_PLUGIN_API_SURFACE,
182
+ path: path,
183
+ message: "`#{method_name}' is not part of the plugin API: `#{interface.describe}' does not declare it. " \
184
+ "The engine object answers it today, but Rigor does not promise to keep it; call a member the " \
185
+ "interface declares (sig/rigor/plugin/interfaces.rbs) or a public namespace " \
186
+ "(docs/internal-spec/public-api.md)",
187
+ severity: :warning,
188
+ receiver_type: nil,
189
+ method_name: method_name
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -19,7 +19,7 @@ module Rigor
19
19
  # branched on, whose declaration the reader's author never opened. A configuration constant read in ten
20
20
  # files would put a warning in all ten.
21
21
  #
22
- # This is [ADR-58](../../../../docs/adr/58-declaration-sourced-ivar-typing.md)'s discipline applied to a
22
+ # This is [ADR-58](../../../../docs/adr/58-ivar-field-typing.md)'s discipline applied to a
23
23
  # different carrier: "a consumer may only ever *withhold* a firing it would otherwise make, so the mark
24
24
  # can lose precision but can never manufacture a false positive"
25
25
  # (`docs/internal-spec/inference-engine.md` § "Carrier and discipline"). The carrier is deliberately NOT
@@ -115,15 +115,19 @@ module Rigor
115
115
  # exemption exactly rather than on the last segment. A path that renders no qualified name falls back
116
116
  # to its own last segment, which is all there is to ask about.
117
117
  def published_path?(node, scope)
118
- rendered = Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
118
+ rendered = constant_spelling(node)
119
119
  !rendered.nil? && scope.published_constant?(rendered)
120
120
  end
121
121
 
122
122
  # The one hop. Resolves the callee through the ordinary `Scope` accessors (so the ADR-46 recorder sees
123
123
  # the read and the caller gains an edge to the callee — the conservative direction) and scans its
124
124
  # body. Fails soft to false: a resolution this cannot make is not a reason to withhold.
125
+ #
126
+ # Issue #3199 — the empty-table shortcut is taken only when no ADR-46 recorder is watching: the scan's
127
+ # `Scope#published_constant?` questions are what record the `constant:` edges, and a reader that skipped
128
+ # them would not be re-checked when another file publishes a constant the callee's body names.
125
129
  def callee_reads_published_constant?(node, scope)
126
- return false if scope.published_constant_names.empty?
130
+ return false if scope.published_constant_names.empty? && !Analysis::DependencyRecorder.recording?
127
131
 
128
132
  def_node = resolve_callee(node, scope)
129
133
  return false if def_node.nil?
@@ -172,10 +176,30 @@ module Rigor
172
176
  end
173
177
  end
174
178
 
175
- # An explicit worklist rather than recursion, so the node budget is one loop-carried local instead of
176
- # a shared mutable cell (the engine value-pins a one-element Array counter and then self-flags the
177
- # guard's own `negative?` as an always-falsey condition — this rule's own medicine).
178
179
  def body_reads_published_constant?(root, scope)
180
+ scan_body(root) { |node| return true if published_constant_node?(node, scope) }
181
+ false
182
+ end
183
+
184
+ # Issue #3186 — the constant spellings the one hop's body scan would ask `Scope#published_constant?` about,
185
+ # sorted and de-duplicated: every node {#body_reads_published_constant?} visits within its budget, in its
186
+ # order. The hop's answer for any reader is a function of this list and of that reader's publication
187
+ # tables, so the ADR-89 WD2 gate stores it beside a def's return summary and compares it on an edit: a
188
+ # reader the gate skips would otherwise keep a withholding (or a firing) the edited body no longer earns.
189
+ def body_constant_spellings(root)
190
+ spellings = []
191
+ scan_body(root) do |node|
192
+ spelling = constant_spelling(node)
193
+ spellings << spelling unless spelling.nil?
194
+ end
195
+ spellings.uniq.sort
196
+ end
197
+
198
+ # Yields each node of `root` the hop's scan visits, stopping once {BODY_SCAN_BUDGET} is spent. An explicit
199
+ # worklist rather than recursion, so the node budget is one loop-carried local instead of a shared mutable
200
+ # cell (the engine value-pins a one-element Array counter and then self-flags the guard's own `negative?`
201
+ # as an always-falsey condition — this rule's own medicine).
202
+ def scan_body(root)
179
203
  stack = [root]
180
204
  budget = BODY_SCAN_BUDGET
181
205
  until stack.empty?
@@ -183,19 +207,24 @@ module Rigor
183
207
  next unless node.is_a?(Prism::Node)
184
208
 
185
209
  budget -= 1
186
- return false if budget.negative?
187
- return true if published_constant_node?(node, scope)
210
+ return if budget.negative?
188
211
 
212
+ yield node
189
213
  node.rigor_each_child { |child| stack << child }
190
214
  end
191
- false
192
215
  end
193
216
 
194
217
  def published_constant_node?(node, scope)
218
+ spelling = constant_spelling(node)
219
+ !spelling.nil? && scope.published_constant?(spelling)
220
+ end
221
+
222
+ # The name a constant reference asks the publication tables about ({#published_path?}'s rendering for a
223
+ # path), or nil for any other node and for a path that renders no name.
224
+ def constant_spelling(node)
195
225
  case node
196
- when Prism::ConstantReadNode then scope.published_constant?(node.name.to_s)
197
- when Prism::ConstantPathNode then published_path?(node, scope)
198
- else false
226
+ when Prism::ConstantReadNode then node.name.to_s
227
+ when Prism::ConstantPathNode then Source::ConstantPath.qualified_name_or_nil(node) || node.name&.to_s
199
228
  end
200
229
  end
201
230
 
@@ -36,10 +36,36 @@ module Rigor
36
36
  MISSING_HOOK = :method_missing
37
37
  # The roots whose project mixins `Inference::ObjectMixins` answers for.
38
38
  OBJECT_ROOTS = %w[Object Kernel BasicObject].to_set.freeze
39
- private_constant :MISSING_HOOK, :OBJECT_ROOTS
39
+ # Issue #2792 — the owners of a `method_missing` every object (and every class object) reaches. Core's
40
+ # `BasicObject#method_missing` raises `NoMethodError`, so it opens nothing; core declares none on the others,
41
+ # which are here so that a project or gem RBS reopening `Object`, `Kernel`, `Module` or `Class` with one cannot
42
+ # silence the rule for every receiver.
43
+ UNIVERSAL_HOOK_OWNERS = {
44
+ instance: OBJECT_ROOTS,
45
+ singleton: (OBJECT_ROOTS | %w[Module Class]).freeze
46
+ }.freeze
47
+ private_constant :MISSING_HOOK, :OBJECT_ROOTS, :UNIVERSAL_HOOK_OWNERS
40
48
 
41
49
  module_function
42
50
 
51
+ # Issue #2792 — whether RBS gives `class_name`'s `side` a `method_missing` from somewhere other than the
52
+ # universal roots: the class itself, an RBS ancestor or an included module (`Delegator` for a
53
+ # `SimpleDelegator`, `class W < SimpleDelegator`, `OpenStruct`). Ruby sends any name the receiver lacks there,
54
+ # private or public, so no name is provably missing. Only the absence verdict reads it: the call's type stays
55
+ # what an unresolved call answers.
56
+ def rbs_missing_hook?(scope, class_name, side)
57
+ definition =
58
+ if side == :singleton
59
+ Rigor::Reflection.singleton_method_definition(class_name, MISSING_HOOK, scope: scope)
60
+ else
61
+ Rigor::Reflection.instance_method_definition(class_name, MISSING_HOOK, scope: scope)
62
+ end
63
+ owner = definition&.defined_in
64
+ return false if owner.nil?
65
+
66
+ !UNIVERSAL_HOOK_OWNERS.fetch(side).include?(owner.to_s.delete_prefix("::"))
67
+ end
68
+
43
69
  # Whether the project may answer `method_name` on an instance of `class_name` through one of its RBS
44
70
  # ancestors.
45
71
  def ancestor_may_answer?(scope, class_name, method_name)
@@ -39,8 +39,8 @@ module Rigor
39
39
  SEND_CALLS = %i[send __send__ public_send].freeze
40
40
  # Reading the block through these runs it with its own `self`, or does not run it.
41
41
  PLAIN_PROC_CALLS = %i[call yield [] === ! arity lambda? parameters nil?].freeze
42
- # Calls, on any receiver, that can reach a local by name: a `Binding`, a string `eval`, and the string forms of
43
- # the `*_eval` family (with an argument), whose code runs in the caller's scope.
42
+ # Calls, on any receiver, that can reach a local by name: `binding`, `eval`, `local_variable_get`. The string
43
+ # forms of the `*_eval` family (`STRING_EVAL_CALLS`, with an argument) run in the caller's scope too.
44
44
  BINDING_CALLS = %i[binding eval local_variable_get].freeze
45
45
  STRING_EVAL_CALLS = %i[instance_eval class_eval module_eval].freeze
46
46
  private_constant :STRING_EVAL_CALLS
@@ -96,7 +96,7 @@ module Rigor
96
96
  found
97
97
  end
98
98
 
99
- # `{name => {[owner, side] => rows}}` over the project's named `def` rows, built once per file.
99
+ # The project's named `def` row counts, keyed by method name and then by owner and side; built once per file.
100
100
  def def_rows(scope)
101
101
  @def_rows ||= RebindingBlockCallees.def_row_counts(scope)
102
102
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../inference/scope_indexer"
6
+
7
+ module Rigor
8
+ module Analysis
9
+ module CheckRules
10
+ # Issue #3155 — the singleton defs of one file that a later def of the same name certainly replaces, which
11
+ # `def.return-type-mismatch` does not check against the class's signature: Ruby keeps the later body.
12
+ #
13
+ # Both defs must be certain singleton defs of the same class or module body: a direct statement of that body
14
+ # (`def self.x`, or `def K.x` where `K` names the class), or a direct statement of a `class << self` there
15
+ # (`def x`, or `def K.x`). A def in a block (`Class.new do … end`, `Struct.new(…) do … end`), under a condition,
16
+ # loop or `begin`/`rescue`, or in a method body may not run, or runs on another object, so it replaces nothing,
17
+ # and a def written there is never read as replaced either. A reopening in another file is not read.
18
+ #
19
+ # The walk groups defs by the class or module body node only; whether a constant receiver names the class is
20
+ # read from the def's own Scope (`self_type`, the indexer's declaration context), so it derives none itself.
21
+ class ReplacedSingletonDefs
22
+ def initialize(root, scope_index)
23
+ @root = root
24
+ @scope_index = scope_index
25
+ end
26
+
27
+ def replaced?(def_node) = offsets.include?(def_node.location.start_offset)
28
+
29
+ # `def (self).x` and `def (K).x` name the receiver inside parentheses.
30
+ def self.unparenthesized(node)
31
+ while node.is_a?(Prism::ParenthesesNode)
32
+ body = node.body
33
+ node = body.is_a?(Prism::StatementsNode) && body.body.size == 1 ? body.body.first : body
34
+ end
35
+ node
36
+ end
37
+
38
+ private
39
+
40
+ def offsets
41
+ @offsets ||= collect
42
+ end
43
+
44
+ def collect
45
+ replaced = Set.new
46
+ each_class_body(@root) do |body|
47
+ last = {}
48
+ certain_singleton_defs(body).each do |def_node|
49
+ replaced << last[def_node.name].location.start_offset if last.key?(def_node.name)
50
+ last[def_node.name] = def_node
51
+ end
52
+ end
53
+ replaced
54
+ end
55
+
56
+ def each_class_body(node, &)
57
+ return if node.nil?
58
+
59
+ yield node.body if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
60
+ node.rigor_each_child { |child| each_class_body(child, &) }
61
+ end
62
+
63
+ def certain_singleton_defs(body)
64
+ statements(body).flat_map do |stmt|
65
+ case stmt
66
+ when Prism::DefNode
67
+ lexical_self?(stmt.receiver, stmt, self_allowed: true) ? [stmt] : []
68
+ when Prism::SingletonClassNode
69
+ next [] unless lexical_self?(stmt.expression, stmt, self_allowed: true)
70
+
71
+ statements(stmt.body).select do |inner|
72
+ inner.is_a?(Prism::DefNode) &&
73
+ (inner.receiver.nil? || lexical_self?(inner.receiver, inner, self_allowed: false))
74
+ end
75
+ else
76
+ []
77
+ end
78
+ end
79
+ end
80
+
81
+ # `at` is the node whose Scope names the class: its `self_type` is the class or its singleton.
82
+ def lexical_self?(node, at, self_allowed:)
83
+ node = self.class.unparenthesized(node)
84
+ return self_allowed if node.is_a?(Prism::SelfNode)
85
+
86
+ self_type = @scope_index[at]&.self_type
87
+ return false unless self_type.respond_to?(:class_name)
88
+
89
+ Inference::ScopeIndexer.def_receiver_targets_lexical_self?(node, self_type.class_name.split("::"))
90
+ end
91
+
92
+ def statements(body) = body.is_a?(Prism::StatementsNode) ? body.body : []
93
+ end
94
+ end
95
+ end
96
+ end
@@ -55,8 +55,7 @@ module Rigor
55
55
  collect_ensure_returns(begin_node)
56
56
  end
57
57
 
58
- # Returns `Array<{return_node:}>` — one entry per offending `return`, frozen the same way
59
- # `#collect` returns it.
58
+ # One `{return_node:}` entry per offending `return`, frozen the same way `#collect` returns them.
60
59
  def results
61
60
  @results.freeze
62
61
  end
@@ -81,6 +81,12 @@ module Rigor
81
81
  # Issue #1692 (the ADR-47 WD5 amendment) — a call to the `ruby2_keywords` family Ruby 4.1 deprecates. Runs only
82
82
  # under an explicit `target_ruby` of 4.1 or later; see {RubyDeprecations}.
83
83
  RULE_DEPRECATED_RUBY2_KEYWORDS = "call.deprecated-ruby2-keywords"
84
+ # ADR-128 WD6 / ADR-130 slice 2 (#2686) — a call in plugin source on a `Rigor::Plugin` interface value to a
85
+ # member the interface does not declare. Authored `:warning`, resolved `:off` by every profile and promoted
86
+ # only by the `plugin-api-surface` bleeding-edge feature. The one built-in id under `plugin.`, the prefix a
87
+ # plugin's own rows carry as `plugin.<id>.<rule>`: it is in {ALL_RULES}, so `disable:`, markers and
88
+ # `severity_overrides:` resolve it as a catalogue id before any plugin-id reading.
89
+ RULE_PLUGIN_API_SURFACE = "plugin.api-surface"
84
90
 
85
91
  ALL_RULES = [
86
92
  RULE_UNDEFINED_METHOD,
@@ -117,7 +123,8 @@ module Rigor
117
123
  RULE_EFFECT_LISKOV_WIDENED,
118
124
  RULE_EFFECT_UNKNOWN_LABEL,
119
125
  RULE_EFFECT_ANNOTATIONS_UNCHECKED,
120
- RULE_CONTRADICTING_SIGNATURE
126
+ RULE_CONTRADICTING_SIGNATURE,
127
+ RULE_PLUGIN_API_SURFACE
121
128
  ].freeze
122
129
 
123
130
  # Backward-compat alias table (ADR-8 § "Backward compatibility"). Existing user code with
@@ -178,7 +185,7 @@ module Rigor
178
185
 
179
186
  # Resolves a user-supplied rule token (`undefined-method`, `call.undefined-method`, or the family wildcard
180
187
  # `call`) to the set of canonical rule identifiers it disables. Returns `nil` for `"all"` (the existing
181
- # wildcard meaning "every rule"), or for unknown tokens.
188
+ # wildcard meaning "every rule"), and an empty array for an unknown token.
182
189
  def self.resolve_rule_token(token)
183
190
  return nil if token == "all"
184
191
  return [LEGACY_RULE_ALIASES.fetch(token)] if LEGACY_RULE_ALIASES.key?(token)
@@ -10,7 +10,7 @@ module Rigor
10
10
  module CheckRules
11
11
  # ADR-53 Track B — the engine-owned AST walk that hosts CheckRules' per-node collectors, so a file is
12
12
  # traversed once for all of them instead of once per collector (the ADR-52 WD4 model applied to the
13
- # built-in rules; this walk and {Plugin::NodeRuleWalk} converge in slice B4).
13
+ # built-in rules; {Plugin::NodeRuleWalk} drives the same collectors, ADR-53 B4).
14
14
  #
15
15
  # The walk is a single visit-before-descend DFS over `compact_child_nodes`. It threads the union of the
16
16
  # context the hosted collectors need — the per-collector hand-rolled walks each tracked some subset of
@@ -146,12 +146,14 @@ module Rigor
146
146
  qualified_prefix = extend_prefix(node, context.qualified_prefix)
147
147
  detached_ivar_facet = context.detached_ivar_facet || detached_ivar_facet?(node)
148
148
 
149
- Context.new(
150
- in_loop_or_block: in_loop_or_block,
151
- qualified_prefix: qualified_prefix,
152
- inside_def: inside_def,
153
- detached_ivar_facet: detached_ivar_facet
154
- )
149
+ # Nearly every node leaves all four fields as they were, and a {Context} is immutable, so the parent's
150
+ # is the child's. This runs once per node and was a top allocation site (a Data per node).
151
+ if in_loop_or_block == context.in_loop_or_block && inside_def == context.inside_def &&
152
+ qualified_prefix.equal?(context.qualified_prefix) && detached_ivar_facet == context.detached_ivar_facet
153
+ return context
154
+ end
155
+
156
+ Context.new(in_loop_or_block, qualified_prefix, inside_def, detached_ivar_facet)
155
157
  end
156
158
 
157
159
  # True for a node whose descendants write ivars to a store the enclosing class name does not name:
@@ -123,10 +123,9 @@ module Rigor
123
123
  @scope_index[node] || @scope_index[node.statements] || @scope_index[node.rescue_clause]
124
124
  end
125
125
 
126
- # Resolves one clause to `{clause:, names: [String, …]}` where `names` are the certified exception
127
- # CLASS names it rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat /
128
- # module — the comparisons involving it stay silent). A bare `rescue` names the implicit
129
- # `StandardError`.
126
+ # Resolves one clause to `{clause:, names:}`, where `names` are the certified exception CLASS names it
127
+ # rescues, or nil when the clause is opaque (any entry unresolved / dynamic / splat / module — the
128
+ # comparisons involving it stay silent). A bare `rescue` names the implicit `StandardError`.
130
129
  def clause_info(clause, prefix, scope)
131
130
  exceptions = clause.exceptions
132
131
  if exceptions.empty?
@@ -9,9 +9,9 @@ module Rigor
9
9
  # the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
10
10
  #
11
11
  # The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
12
- # `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
13
- # declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
14
- # that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
12
+ # `$;` is declared as a Regexp, a String or nil, but its setter also takes any object with `to_str`, and
13
+ # `$stdout` is declared as an IO, but its setter takes anything that responds to `write`. Each entry names
14
+ # the CRuby function that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
15
15
  #
16
16
  # A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
17
17
  # (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
@@ -280,8 +280,8 @@ module Rigor
280
280
 
281
281
  # The trailing `else` is dead when every subject value is covered by the `when` clauses — the final
282
282
  # falsey scope (recorded on the `else` node) narrows the subject to `bot`. Defensive `else` bodies (a
283
- # bare `raise` / `fail` / `throw`) are deliberate guards, not removable dead code, so they are
284
- # skipped.
283
+ # bare `raise` / `fail` / `throw` / `abort` / `exit`) are deliberate guards, not removable dead code, so
284
+ # they are skipped.
285
285
  def collect_else(else_clause, subject_name, keyword)
286
286
  return if else_clause.nil? || else_clause.statements.nil?
287
287
  return if defensive_else?(else_clause)
@@ -29,7 +29,7 @@ module Rigor
29
29
  class VoidValueUseCollector
30
30
  # The assignment forms whose `value` slot is a value position. `MultiWriteNode` (destructuring) is
31
31
  # intentionally excluded — its `value` is a container the void call would have to be spread through,
32
- # not a direct use, so it is out of this slice's FP-narrow scope.
32
+ # not a direct use, so it is outside the FP-narrow scope.
33
33
  WRITE_NODE_CLASSES = [
34
34
  Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
35
35
  Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode,
@@ -100,7 +100,7 @@ module Rigor
100
100
  return unless value_node.is_a?(Prism::CallNode)
101
101
 
102
102
  scope = @scope_index[value_node]
103
- origin = scope&.void_origins&.[](value_node)
103
+ origin = scope&.void_origin(value_node)
104
104
  return if origin.nil?
105
105
 
106
106
  @results << Result.new(void_node: value_node, origin: origin)