rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "multi_target_binder"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The auto-splat half of {BlockParameterBinder}: given a block's positional parameter shape and the one
9
+ # value the receiver yields, it answers the positional table the block's names are bound from.
10
+ #
11
+ # Ruby blocks (NOT lambdas) spread a single yielded array-ish value across their positionals, and CRuby
12
+ # decides that on the PARAMETER LIST alone ({ParameterShape#splats?}) — never on the value. So the
13
+ # carrier only decides how precisely each position can be filled, and a carrier this module cannot
14
+ # decompose lowers the positions to `Dynamic[Top]` rather than cancelling the spread (issue #1116).
15
+ #
16
+ # Each member of the carrier (a union distributes; anything else is its own single member) contributes
17
+ # one {Table}, and {join} folds them into the answer:
18
+ #
19
+ # - a `Type::Tuple` spreads element-wise, leading positionals (required, then optional) from the head
20
+ # and, with a rest present, trailing positionals from the tail with the rest absorbing the middle —
21
+ # the split `MultiTargetBinder` applies to `a, *r, b = tuple`, so `|*r, v|` over `[K, V]` binds `v` to
22
+ # `V`. A slot past the tuple is left unfilled and lands on `Dynamic[Top]`;
23
+ # - an `Array[T]` ({MultiTargetBinder.array_element_type}) binds `T` to every required and trailing
24
+ # positional, `Dynamic[Top]` to every optional one (a short array hands it its default, not `nil`),
25
+ # and `Array[T]` to a named rest. A short array pads the fixed positions with `nil` at runtime, so
26
+ # those positions are reported optimistic (issue #1093);
27
+ # - an opaque array carrier — a raw `Array`, an `Array[untyped]` / `Array[top]`, a `Refined` /
28
+ # `Difference` over either, or a `Dynamic` over any `Array` however precise — binds `Dynamic[Top]` to
29
+ # every position: the floor `MultiTargetBinder` binds for the same undecomposable right-hand side, and
30
+ # the reason `[1, 2].tap { |a, b| a.succ }` no longer reads `a` as the whole `Array` (issue #1116);
31
+ # - a bare `nil` binds `nil` to every position. `nil` has no `to_ary`, so CRuby passes it as the one
32
+ # argument and `|a, b|` sees `nil, nil` — which is also what the wrap `[nil]` pads to, so the two
33
+ # readings agree here, and {join} takes that `nil` back out of any position another member fills;
34
+ # - every other member takes the same `Dynamic[Top]` floor as the opaque one, and does NOT license the
35
+ # spread on its own. It is the member the union knows least about, and joining it as a value would put
36
+ # a nil-bearing element beside a nil-free one: `String#scan`'s `String | Array[String?]` yield joins a
37
+ # capture's `String?` against the whole match's `String` and fires `call.possible-nil-receiver` on the
38
+ # correlated-invariant `refinements[name.to_sym]`, which ADR-5 ranks above the precision.
39
+ #
40
+ # The spread needs at least one Tuple, `Array[T]` or opaque array member; a carrier with none of them
41
+ # ({#for} answers nil) leaves the binder's declared types alone, which binds the value to the first
42
+ # parameter and `Dynamic[Top]` after. So a lone value with no `to_ary` is NOT wrapped as `[value]` the
43
+ # way `a, b = value` wraps it: the one-value premise of auto-splat is the RBS block signature's arity,
44
+ # which nothing here can check against the actual yield, and a wrong wrap binds `nil` to a parameter the
45
+ # block receives a value in.
46
+ #
47
+ # See docs/internal-spec/inference-engine.md for the binding contract.
48
+ module BlockAutoSplat
49
+ # The positional part of a parameter list, as counts: all the auto-splat rule reads. An explicit
50
+ # `BlockParametersNode` list and a numbered-parameter block (`maximum` required positionals) both
51
+ # reduce to it, so the two spellings cannot drift.
52
+ ParameterShape = Data.define(:required, :optional, :post, :rest) do
53
+ def self.of(params_node)
54
+ new(required: params_node.requireds.size, optional: params_node.optionals.size,
55
+ post: params_node.posts.size, rest: !params_node.rest.nil?)
56
+ end
57
+
58
+ # The `_1` / `_2` form, read as the explicit list of `arity` required positionals.
59
+ def self.of_arity(arity)
60
+ new(required: arity, optional: 0, post: 0, rest: false)
61
+ end
62
+
63
+ def positional_count = required + optional + post
64
+
65
+ # CRuby's own condition (`vm_callee_setup_block_arg` / `setup_parameters_complex`): a block splats a
66
+ # lone array argument when it has a mandatory positional (`lead + post > 0`) or more than one
67
+ # optional, except a bare `|a|` (the iseq's `ambiguous_param0`). So `|k, *r|`, `|*r, v|`,
68
+ # `|a = 1, b = 2|` and the trailing-comma `|k,|` (Prism's `ImplicitRestNode`) splat, while `|*r|`,
69
+ # `|a = 1, *r|` and `|a, &b|` / `|a, k: 1|` do not.
70
+ def splats?
71
+ mandatory = required + post
72
+ return false unless mandatory.positive? || optional > 1
73
+
74
+ !(mandatory == 1 && optional.zero? && !rest)
75
+ end
76
+ end
77
+
78
+ # A positional table: the type per position, the positions bound optimistically, and the named rest's
79
+ # type (nil for the `Array[Dynamic[Top]]` default). One arm produces one; {join} folds them into one.
80
+ # Private as a name — {#for} hands the folded value out, and nothing outside constructs one.
81
+ Table = Data.define(:types, :optimistic_positions, :rest_type)
82
+ private_constant :Table
83
+
84
+ # The arm kinds that make a member an array as far as the spread is concerned, so that a union
85
+ # carrying one of them spreads even where its other members do not decompose.
86
+ ARRAY_CARRIER_ARMS = %i[tuple array opaque].freeze
87
+ private_constant :ARRAY_CARRIER_ARMS
88
+
89
+ module_function
90
+
91
+ # @param shape — the block's {ParameterShape}.
92
+ # @param carrier — the one value the receiver yields.
93
+ # @return the folded {Table}, or nil when no member is an array carrier and the
94
+ # spread must leave the declared types alone.
95
+ def for(shape, carrier)
96
+ arms = arms_of(carrier)
97
+ return nil if arms.nil?
98
+
99
+ join(arms.map { |member, kind| table_for(shape, member, kind) })
100
+ end
101
+
102
+ def arms_of(carrier)
103
+ members = carrier.is_a?(Type::Union) ? carrier.members : [carrier]
104
+ arms = members.map { |member| [member, arm_of(member)] }
105
+ arms.any? { |_, kind| ARRAY_CARRIER_ARMS.include?(kind) } ? arms : nil
106
+ end
107
+
108
+ def arm_of(member)
109
+ return :tuple if member.is_a?(Type::Tuple)
110
+ return :array if MultiTargetBinder.array_element_type(member)
111
+ return :opaque if opaque_array_carrier?(member)
112
+ return :nil_value if nil_literal?(member)
113
+
114
+ :unknown
115
+ end
116
+
117
+ # A carrier that is an array the module may not decompose into per-position types: the raw `Array`, an
118
+ # `Array[untyped]` / `Array[top]`, a `Refined` / `Difference` over either, and a `Dynamic` over ANY
119
+ # carrier that would license the spread — `Dynamic[Array[Integer]]`, `Dynamic[[Integer, String]]`,
120
+ # `Dynamic[Array[Integer] | nil]` alike. {MultiTargetBinder.array_element_type} declines every
121
+ # `Dynamic` wrapper on purpose: the value is gradual, and projecting its static facet would hand each
122
+ # position a bare `T` that licenses the negative rules (issue #1093, ADR-5). That is a reason to floor
123
+ # the positions, not a reason to leave the whole value on the first parameter, so the facet decides
124
+ # only whether CRuby would splat.
125
+ def opaque_array_carrier?(type)
126
+ case type
127
+ when Type::Nominal then type.class_name == "Array"
128
+ when Type::Refined, Type::Difference then opaque_array_carrier?(type.base)
129
+ when Type::Dynamic then !arms_of(type.static_facet).nil?
130
+ else false
131
+ end
132
+ end
133
+
134
+ def table_for(shape, member, kind)
135
+ case kind
136
+ when :tuple then tuple_table(shape, member.elements)
137
+ when :array then array_table(shape, MultiTargetBinder.array_element_type(member))
138
+ when :nil_value then uniform_table(shape, member)
139
+ else uniform_table(shape, Type::Combinator.untyped)
140
+ end
141
+ end
142
+
143
+ # The split is mirrored rather than delegated to `MultiTargetBinder.decompose_tuple` because that one
144
+ # pads a missing slot with `Constant[nil]` and softens `X | nil`; a block slot past the tuple has
145
+ # always bound `Dynamic[Top]`. Without a rest the trailing positionals continue from the head (Ruby
146
+ # fills `|a, b = 1, c|` from the head when the tuple is long enough).
147
+ def tuple_table(shape, elements)
148
+ head = shape.required + shape.optional
149
+ posts = shape.post
150
+ tail_start = shape.rest ? [elements.size - posts, head].max : head
151
+ types = Array.new(head) { |i| elements[i] } + Array.new(posts) { |j| elements[tail_start + j] }
152
+ Table.new(types: types, optimistic_positions: [], rest_type: nil)
153
+ end
154
+
155
+ def array_table(shape, element)
156
+ required = shape.required
157
+ optional = shape.optional
158
+ posts = shape.post
159
+ Table.new(
160
+ types: Array.new(required) { element } + Array.new(optional) { Type::Combinator.untyped } +
161
+ Array.new(posts) { element },
162
+ optimistic_positions: (0...required).to_a + ((required + optional)...(required + optional + posts)).to_a,
163
+ rest_type: Type::Combinator.nominal_of("Array", type_args: [element])
164
+ )
165
+ end
166
+
167
+ # One type for every position: the `Dynamic[Top]` floor of a member the module cannot decompose, or
168
+ # the `nil` a `nil` member hands each of them. The `nil` rest_type leaves a named rest on the binder's
169
+ # `Array[Dynamic[Top]]` default, which is right for both — CRuby gives `nil.tap { |a, *r| }` an empty
170
+ # `r`, and an opaque carrier says nothing about the leftovers.
171
+ def uniform_table(shape, type)
172
+ Table.new(types: Array.new(shape.positional_count) { type }, optimistic_positions: [], rest_type: nil)
173
+ end
174
+
175
+ # A single member's table is used as is. Across members, a position any member leaves unfilled (a slot
176
+ # past a short Tuple) or fills with `Dynamic[Top]` stays `Dynamic[Top]`, and a member's bare `nil`
177
+ # drops out of a position another member fills with a value, that position then being optimistic —
178
+ # `MultiTargetBinder`'s cross-member softening. A named rest joins only when every member supplies
179
+ # one; otherwise the binder's `Array[Dynamic[Top]]` default stands.
180
+ def join(tables)
181
+ types, softened = join_types(tables.map(&:types))
182
+ rests = tables.map(&:rest_type)
183
+ Table.new(
184
+ types: types,
185
+ optimistic_positions: (tables.flat_map(&:optimistic_positions) + softened).uniq,
186
+ rest_type: rests.include?(nil) ? nil : Type::Combinator.union(*rests)
187
+ )
188
+ end
189
+
190
+ # @return the joined table and the positions it softened.
191
+ def join_types(tables)
192
+ return [tables.first, []] if tables.size == 1
193
+
194
+ softened = []
195
+ joined = Array.new(tables.map(&:size).max) do |i|
196
+ column = tables.map { |table| table[i] }
197
+ next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
198
+
199
+ firm = column.reject { |t| nil_literal?(t) }
200
+ next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
201
+
202
+ softened << i
203
+ Type::Combinator.union(*firm)
204
+ end
205
+ [joined, softened]
206
+ end
207
+
208
+ def nil_literal?(type) = type.is_a?(Type::Constant) && type.value.nil?
209
+
210
+ # {#for} is the whole surface; the arms and the fold are this module's own business, and keeping them
211
+ # off the singleton stops a caller reaching past the fold for one member's table.
212
+ private_class_method :arms_of, :arm_of, :opaque_array_carrier?, :table_for, :tuple_table, :array_table,
213
+ :uniform_table, :join, :join_types, :nil_literal?
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "../reflection"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #1095 — the first slice of the block call-timing summary that
9
+ # `docs/type-specification/control-flow-analysis.md` § "Block call timing" asks for: which callees invoke
10
+ # their block **immediately, exactly once, and before they return**.
11
+ #
12
+ # The fact matters for the value of the call. Since #853 a block-level `break` joins the yielding call's
13
+ # type, unioned with what the callee returns when the block completes normally. For an arbitrary callee
14
+ # that normal return must stay — `each` may never yield on an empty receiver. For a callee that always
15
+ # yields exactly once before returning, a block whose normal completion is unreachable means the normal
16
+ # return is unreachable too, so the call's value is its `break` arms alone (`bot` when there are none):
17
+ # `[1, 2].tap { break "s" }` is `"s"`, `[1, 2].tap { raise "x" }` is `bot`.
18
+ #
19
+ # ## Catalogue
20
+ #
21
+ # Like {ClosureEscapeAnalyzer}'s `OBJECT_NON_ESCAPING` — a weaker "block is not retained" fact that `each`
22
+ # and `map` also satisfy — this ships as a hardcoded table keyed by `(owner, method)`, where the owner is
23
+ # the module whose declaration Ruby dispatches to. The sub-phase that replaces the escape catalogue with an
24
+ # `RBS::Extended` call-timing effect (`rbs-extended.md` reserves the bundle slot) is expected to absorb
25
+ # this table in the same move, which is why it is keyed by the declaring owner, the way a signature-borne
26
+ # effect would be, rather than by the receiver's class.
27
+ #
28
+ # The owner is `Kernel` for all three, in CRuby (`Object.instance_method(:tap).owner`) and in the bundled
29
+ # core RBS alike. `Object` is deliberately NOT accepted: every class inherits these methods through
30
+ # `Object`, so a declaration that resolves to `Object` itself is a project or gem override written on top
31
+ # of Kernel's, and that override is not known to yield exactly once.
32
+ #
33
+ # The summary is a pure query. It never raises on unrecognised input and answers `false` — keep the
34
+ # `break | normal-return` union — whenever it cannot prove the call reaches the catalogued declaration.
35
+ module BlockCallTiming
36
+ EXACTLY_ONCE_IMMEDIATE = {
37
+ "Kernel" => %i[tap then yield_self].freeze
38
+ }.freeze
39
+
40
+ CANDIDATE_NAMES = EXACTLY_ONCE_IMMEDIATE.values.flatten.uniq.freeze
41
+ private_constant :CANDIDATE_NAMES
42
+
43
+ # The roots a project monkey-patch would redefine the method on for every receiver at once.
44
+ PATCHABLE_ROOTS = %w[Object Kernel BasicObject].freeze
45
+ private_constant :PATCHABLE_ROOTS
46
+
47
+ # A class object's own metaclass ancestry adds these ahead of `Object`, so a patch on either one
48
+ # redefines the method for every class-object receiver.
49
+ SINGLETON_ROOTS = %w[Class Module].freeze
50
+ private_constant :SINGLETON_ROOTS
51
+
52
+ # Receiver-less (or `self.` / `Kernel.`) calls that never return normally: they raise, throw, or end
53
+ # the process. Anything else — including `loop`, whose RBS return is `bot` although a `StopIteration`
54
+ # ends it normally ({.loop_may_complete?}) — proves nothing.
55
+ NON_RETURNING_CALLS = %i[raise fail throw exit exit! abort].freeze
56
+ private_constant :NON_RETURNING_CALLS
57
+
58
+ # Issue #1107 — the nodes a `loop` body may consist of while still provably unable to raise
59
+ # `StopIteration`: none of them dispatches a method, so nothing in the body can run code that raises.
60
+ # Everything else — every call (operators and `[]` included), `yield`, `super`, interpolation, a splat,
61
+ # a constant read (`const_missing`), a `rescue` — may, and declines.
62
+ STOP_ITERATION_FREE_NODES = [
63
+ Prism::StatementsNode, Prism::ParenthesesNode, Prism::ArgumentsNode,
64
+ Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::ReturnNode,
65
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
66
+ Prism::StringNode, Prism::SymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::SelfNode,
67
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode,
68
+ Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode,
69
+ Prism::IfNode, Prism::UnlessNode, Prism::ElseNode, Prism::AndNode, Prism::OrNode, Prism::ArrayNode
70
+ ].freeze
71
+ private_constant :STOP_ITERATION_FREE_NODES
72
+
73
+ module_function
74
+
75
+ # Issue #1107 — whether a `loop` call can complete normally although its declared return is `bot`.
76
+ #
77
+ # `Kernel#loop` is `() { () -> void } -> bot` in core RBS, yet it rescues a `StopIteration` its block
78
+ # raises and returns that exception's `result` — the enumerator-draining idiom `loop { out << e.next }`
79
+ # returns `e`'s own `each` value. So the declared `bot` holds only for a body that provably cannot raise
80
+ # `StopIteration`: one built from {STOP_ITERATION_FREE_NODES} alone (`loop {}`, `loop { break 1 }`), in a
81
+ # block that declares no parameters. A `&blk` block-pass has no body to prove anything about and completes
82
+ # too.
83
+ #
84
+ # Gated on the spellings that reach the private `Kernel#loop` — receiver-less, `self.`, `Kernel.`,
85
+ # `::Kernel.` — so `obj.loop { ... }` is some other method, whose declared `bot` is that author's
86
+ # promise. A project redefinition of `loop` itself is not excluded: widening its `bot` costs precision,
87
+ # never a false positive.
88
+ def loop_may_complete?(call_node)
89
+ return false unless call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
90
+
91
+ block = call_node.block
92
+ return false if block.nil?
93
+ return true unless block.is_a?(Prism::BlockNode)
94
+ # A parameter default (`|v = e.next|`) is evaluated on every iteration, since `loop` yields no
95
+ # arguments. Rather than walk the parameter list, any declared parameter widens.
96
+ return true if block.parameters
97
+ return false if block.body.nil?
98
+
99
+ may_raise_stop_iteration?(block.body)
100
+ end
101
+
102
+ def may_raise_stop_iteration?(node)
103
+ return true unless STOP_ITERATION_FREE_NODES.any? { |klass| node.is_a?(klass) }
104
+
105
+ node.compact_child_nodes.any? { |child| may_raise_stop_iteration?(child) }
106
+ end
107
+
108
+ # Cheap name-only pre-gate, so a call that cannot be catalogued pays nothing further.
109
+ def candidate_name?(method_name)
110
+ CANDIDATE_NAMES.include?(method_name)
111
+ end
112
+
113
+ def exactly_once_owner?(owner_name, method_name)
114
+ methods = EXACTLY_ONCE_IMMEDIATE[owner_name.to_s.delete_prefix("::")]
115
+ methods ? methods.include?(method_name) : false
116
+ end
117
+
118
+ # Whether a call of `method_name` on `receiver_type` reaches a catalogued exactly-once declaration.
119
+ #
120
+ # Gated on the RESOLVED owner, not the name: a class that defines its own `tap` (in source, in a
121
+ # project `sig/`, or through a project ancestor), a top-level `def tap` (a private `Object` method,
122
+ # ahead of `Kernel` in every MRO) and an `Object` / `Kernel` monkey-patch all decline. A union receiver
123
+ # qualifies only when every member does; `Dynamic` and the other carriers that name no class decline.
124
+ def exactly_once_call?(receiver_type:, method_name:, scope:)
125
+ return false unless candidate_name?(method_name)
126
+ return false if scope.nil? || project_redefines_root?(method_name, scope)
127
+
128
+ targets = receiver_targets(receiver_type)
129
+ return false if targets.nil? || targets.empty?
130
+
131
+ targets.all? { |class_name, kind| resolves_to_catalogue?(class_name, kind, method_name, scope) }
132
+ rescue StandardError
133
+ false
134
+ end
135
+
136
+ # Syntactic proof that a block body cannot complete normally: every path through it ends in a
137
+ # block-level `break` (which targets the yielding call), `return`, `redo`, or a non-returning Kernel call
138
+ # ({NON_RETURNING_CALLS}) — or in an expression that must evaluate one of those first. This is ANDed
139
+ # with the block-return pass's `bot`, never used alone, because a `bot` can also flow out of a callee's
140
+ # declared return that is not a promise the call never completes (`Kernel#loop` is `-> bot`, yet a
141
+ # `StopIteration` from `e.next` ends it normally). A shape the walk does not recognise declines.
142
+ #
143
+ # Only unconditionally evaluated children are descended: a nested block, lambda, `def` or loop is never
144
+ # entered (its body may not run, and it retargets `break`), a call's block is not a child the call
145
+ # promises to run, and `&&` / `||` count only their left operand. A `begin` with `rescue` qualifies only
146
+ # when its body and every rescue clause must exit; an `ensure` that must exit qualifies on its own.
147
+ def never_completes_normally?(node, scope)
148
+ case node
149
+ when Prism::StatementsNode then node.body.any? { |statement| never_completes_normally?(statement, scope) }
150
+ when Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode then true
151
+ when Prism::ParenthesesNode then never_completes_normally?(node.body, scope)
152
+ when Prism::CallNode then call_never_returns?(node, scope)
153
+ when Prism::IfNode, Prism::UnlessNode then conditional_never_completes?(node, scope)
154
+ when Prism::AndNode, Prism::OrNode then never_completes_normally?(node.left, scope)
155
+ when Prism::BeginNode then begin_never_completes?(node, scope)
156
+ when Prism::ArrayNode then node.elements.any? { |element| never_completes_normally?(element, scope) }
157
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
158
+ never_completes_normally?(node.value, scope)
159
+ else false
160
+ end
161
+ end
162
+
163
+ # Deliberately coarse: ANY project definition of the name — top-level, on any class or module, either
164
+ # side, or a `pre_eval:` patch — declines. Resolving the call-site `self`'s ancestry precisely buys
165
+ # nothing for names this rare, and the block-return pass is no independent check here: it types a
166
+ # self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
167
+ # {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
168
+ def project_defines_anywhere?(method_name, scope)
169
+ return true if scope.top_level_def_for(method_name)
170
+ # `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
171
+ # def-node tables still carry it, on both sides.
172
+ return true if [scope.discovered_methods, scope.discovered_def_nodes, scope.discovered_singleton_def_nodes]
173
+ .any? { |tables| tables.any? { |_class_name, table| table.key?(method_name) } }
174
+
175
+ patched = scope.environment&.project_patched_methods
176
+ !patched.nil? && patched.by_key.any? { |(_class_name, name, _kind), _entry| name == method_name }
177
+ end
178
+
179
+ class << self
180
+ private
181
+
182
+ def call_never_returns?(node, scope)
183
+ return true if never_completes_normally?(node.receiver, scope)
184
+ return true if node.arguments&.arguments&.any? { |argument| never_completes_normally?(argument, scope) }
185
+ return false unless NON_RETURNING_CALLS.include?(node.name)
186
+
187
+ kernel_spelled_receiver?(node.receiver) && !project_defines_anywhere?(node.name, scope)
188
+ end
189
+
190
+ # Implicit self, `self.`, or the `Kernel` module itself (`Kernel.` or the root-anchored `::Kernel.`) —
191
+ # the spellings that reach Kernel's function.
192
+ def kernel_spelled_receiver?(receiver)
193
+ case receiver
194
+ when nil, Prism::SelfNode then true
195
+ when Prism::ConstantReadNode then receiver.name == :Kernel
196
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
197
+ else false
198
+ end
199
+ end
200
+
201
+ def conditional_never_completes?(node, scope)
202
+ return true if never_completes_normally?(node.predicate, scope)
203
+
204
+ alternative = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
205
+ return false if node.statements.nil? || alternative.nil?
206
+
207
+ never_completes_normally?(node.statements, scope) && branch_never_completes?(alternative, scope)
208
+ end
209
+
210
+ def branch_never_completes?(branch, scope)
211
+ case branch
212
+ when Prism::ElseNode then never_completes_normally?(branch.statements, scope)
213
+ else never_completes_normally?(branch, scope)
214
+ end
215
+ end
216
+
217
+ def begin_never_completes?(node, scope)
218
+ return true if node.ensure_clause && never_completes_normally?(node.ensure_clause.statements, scope)
219
+ return false unless never_completes_normally?(node.statements, scope)
220
+
221
+ rescue_clause = node.rescue_clause
222
+ while rescue_clause
223
+ return false unless never_completes_normally?(rescue_clause.statements, scope)
224
+
225
+ rescue_clause = rescue_clause.subsequent
226
+ end
227
+ true
228
+ end
229
+
230
+ def project_redefines_root?(method_name, scope)
231
+ return true if scope.top_level_def_for(method_name)
232
+
233
+ patched = scope.environment&.project_patched_methods
234
+ PATCHABLE_ROOTS.any? do |root|
235
+ scope.discovered_method?(root, method_name, :instance) ||
236
+ patched&.lookup(class_name: root, method_name: method_name, kind: :instance)
237
+ end
238
+ end
239
+
240
+ # `[class_name, :instance | :singleton]` pairs the receiver can dispatch through, or nil when some
241
+ # part of it names no class. `Tuple` / `HashShape` project to `Array` / `Hash`, `Constant[v]` through
242
+ # the value's class, the same projection {ClosureEscapeAnalyzer} uses.
243
+ def receiver_targets(receiver_type)
244
+ case receiver_type
245
+ when Type::Union
246
+ members = receiver_type.members.map { |member| receiver_targets(member) }
247
+ members.any?(&:nil?) ? nil : members.flatten(1)
248
+ when Type::Nominal then [[receiver_type.class_name, :instance]]
249
+ when Type::Singleton then [[receiver_type.class_name, :singleton]]
250
+ when Type::Tuple then [["Array", :instance]]
251
+ when Type::HashShape then [["Hash", :instance]]
252
+ when Type::Constant then [[receiver_type.value.class.name, :instance]]
253
+ end
254
+ end
255
+
256
+ def resolves_to_catalogue?(class_name, kind, method_name, scope)
257
+ return false if class_name.nil?
258
+ return false if scope.discovered_method_through_ancestors?(class_name, method_name, kind)
259
+
260
+ if kind == :singleton
261
+ definition = Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
262
+ return false if singleton_ancestor_patched?(class_name, method_name, scope)
263
+
264
+ return declared_on_catalogue?(definition, method_name)
265
+ end
266
+
267
+ definition = Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
268
+ if definition
269
+ return false if rbs_ancestor_patched?(class_name, method_name, scope)
270
+
271
+ return declared_on_catalogue?(definition, method_name)
272
+ end
273
+ return false if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
274
+
275
+ project_class_resolves_to_catalogue?(class_name, method_name, scope)
276
+ end
277
+
278
+ def declared_on_catalogue?(definition, method_name)
279
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
280
+
281
+ exactly_once_owner?(definition.defined_in, method_name)
282
+ end
283
+
284
+ # A project class the RBS environment does not know: its project ancestry was already cleared by the
285
+ # discovery walk, so the answer rests on the ancestors it reaches OUTSIDE the project. Each is asked
286
+ # on its own terms; an ancestor nobody can resolve is uncertainty and declines. No external ancestor
287
+ # at all is the implicit `< Object`, which reaches `Kernel`.
288
+ def project_class_resolves_to_catalogue?(class_name, method_name, scope)
289
+ return false unless scope.known_user_class?(class_name)
290
+
291
+ scope.external_ancestor_name_candidates(class_name).all? do |candidates|
292
+ external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
293
+ end
294
+ end
295
+
296
+ def external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
297
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
298
+ return false if known.nil?
299
+
300
+ definition = Rigor::Reflection.instance_method_definition(known, method_name, scope: scope)
301
+ # A mixin whose RBS does not mention the method contributes nothing; a CLASS that lacks it is a
302
+ # `BasicObject` lineage, where the call does not reach `Kernel` at all.
303
+ return false if rbs_ancestor_patched?(known, method_name, scope)
304
+ return scope.environment.rbs_module?(known) if definition.nil?
305
+
306
+ declared_on_catalogue?(definition, method_name)
307
+ end
308
+
309
+ # The RBS declaration says where the method was DECLARED; a project reopening of a core ancestor
310
+ # (`module Enumerable; def tap = :x; end`) redefines it without touching RBS, and discovery records
311
+ # it under the ancestor's own name. So every RBS ancestor — mixins included — is asked.
312
+ def rbs_ancestor_patched?(class_name, method_name, scope)
313
+ names_patched?(rbs_ancestor_names(class_name, scope), method_name, :instance, scope)
314
+ end
315
+
316
+ # A class object dispatches through its own and its superclasses' singleton methods, then through
317
+ # `Class`, `Module`, `Object`, `Kernel` as instance methods.
318
+ def singleton_ancestor_patched?(class_name, method_name, scope)
319
+ names_patched?(rbs_ancestor_names(class_name, scope), method_name, :singleton, scope) ||
320
+ names_patched?(SINGLETON_ROOTS, method_name, :instance, scope)
321
+ end
322
+
323
+ def names_patched?(names, method_name, kind, scope)
324
+ patched = scope.environment&.project_patched_methods
325
+ names.any? do |name|
326
+ scope.discovered_method?(name, method_name, kind) ||
327
+ patched&.lookup(class_name: name, method_name: method_name, kind: kind)
328
+ end
329
+ end
330
+
331
+ def rbs_ancestor_names(class_name, scope)
332
+ loader = scope.environment&.rbs_loader
333
+ loader ? loader.ancestor_names_for(class_name.to_s) : []
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end