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,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-119 WD2 — the candidate-set read over {Scope::ResolutionChain}, for a reader that can decline. It is
6
+ # internal (not a `Scope` method, so the pinned public surface is unchanged) and answers one of three
7
+ # things about a method name on a class:
8
+ #
9
+ # - {Known} — every candidate gives the same answer to the question asked;
10
+ # - {UNKNOWN} — the chain does not stand for this name (a mark not discharged, a fork, a retro world that
11
+ # answers otherwise), a candidate disagrees, the walk was cut by the budget, an external ancestor ahead of
12
+ # the candidate may answer first (one RBS does not know, or knows and declares the name in; one it knows
13
+ # and whose declaration lacks the name is skipped);
14
+ # - {ABSENT} — no project entry, no external entry and not the implicit `Object` can answer.
15
+ #
16
+ # On the singleton side (ADR-119 WD3, errata 2026-10-08) the chain is `K`'s singleton, its extends' instance
17
+ # chains, then the superclass's singleton chain; an external superclass entry stands for its whole tail and is
18
+ # tested against RBS's singleton side, an extended external module against its instance side, the implicit tail
19
+ # after a project superclass against `Object`'s singleton side. A standing chain is then read past
20
+ # {SingletonHookDecline}' positional decline: a hook's singleton-side edge is recorded on no includer, so the read
21
+ # declines where one could have landed ahead of its last candidate.
22
+ #
23
+ # THE CALL SITE IS A CONTRACT. A result is consumed only by an exhaustive `case/in` that is the call's own
24
+ # direct predicate, with one arm per answer and no `else` or `in _`, and is never stored, returned or
25
+ # truth-tested: {UNKNOWN} and {ABSENT} are both truthy, so any other use folds a decline into a firing arm.
26
+ # `spec/rigor/inference/definer_resolution_case_in_spec.rb` fails on a call site of any other shape.
27
+ #
28
+ # It migrates one question at a time: `SourceArity` (C1b) and the relationship lints (C1c) read the instance
29
+ # side; no production reader reads the singleton side yet (C2-a landed it for C2's typing sites).
30
+ module DefinerResolution
31
+ Known = Data.define(:answer, :owner)
32
+ UnknownResult = Data.define
33
+ AbsentResult = Data.define
34
+ UNKNOWN = UnknownResult.new
35
+ ABSENT = AbsentResult.new
36
+
37
+ # One candidate definer as a per-chain answer function reports it: the `answer` to the question, the
38
+ # `owner` entry's name, the `index` of that entry on the chain and its `side` (`:instance` or `:singleton`).
39
+ Hit = Data.define(:answer, :owner, :index, :side)
40
+
41
+ QUESTIONS = %i[definer visibility arity override own_side].freeze
42
+ NONE = :rigor_definer_resolution_none
43
+ STRAY = :rigor_definer_resolution_stray
44
+ private_constant :NONE, :STRAY
45
+
46
+ module_function
47
+
48
+ # `answer_in` is the question's per-chain answer function: called with a chain and a position, it returns
49
+ # the {Hit} of the first project entry at or after the position that defines `method_name`, or nil. `:definer`
50
+ # and `:visibility` have a default; every other question supplies its own. It is called once per candidate
51
+ # on the chain, and again on the retro world when the chain has one fork, where `from` is mapped to the
52
+ # entry it followed.
53
+ def resolve(scope, class_name, method_name, side, question:, from: 0, &answer_in)
54
+ raise ArgumentError, "unknown question #{question.inspect}" unless QUESTIONS.include?(question)
55
+
56
+ answer_in = override_answer(scope, &answer_in) if question == :override
57
+ answer_in ||= default_answer(scope, method_name, question)
58
+ flavor = question == :arity ? :arity : :methods
59
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
60
+
61
+ hits = candidates(scope, chain, method_name, from, answer_in)
62
+ return UNKNOWN if hits.nil?
63
+
64
+ verdict = chain.settle(scope, outcomes(hits), owner: hits.first&.owner, unknown_for: method_name) do |retro|
65
+ retro_from = retro_position(chain, retro, from)
66
+ outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in))
67
+ end
68
+ return UNKNOWN unless verdict == :chain
69
+ return UNKNOWN if side == :singleton && SingletonHookDecline.decline?(scope, chain, method_name, from, hits)
70
+
71
+ collapse(hits)
72
+ end
73
+
74
+ # The `:override` question's answer function, built from the caller's block: that block is NOT a per-chain
75
+ # function but `|owner_name| value`, called on each project entry in order, and its first non-nil `value` is
76
+ # the entry's answer, as `[owner_name, value]` (the owner is part of the answer so that two worlds naming
77
+ # different parents disagree). It is the override lints' parent read: the nearest project ancestor after the
78
+ # class whose block answers.
79
+ def override_answer(scope)
80
+ lambda do |chain, from|
81
+ first_hit(chain, scope, from) do |entry|
82
+ value = yield(entry.name)
83
+ [entry.name, value] unless value.nil?
84
+ end
85
+ end
86
+ end
87
+
88
+ # `from` is a position on `chain`; the same position on the retro world is just after the entry it followed.
89
+ # An entry the chain carries twice cannot be matched, which declines.
90
+ def retro_position(chain, retro, from)
91
+ return 0 if from.zero?
92
+
93
+ entry = chain.entries[from - 1]
94
+ return nil if entry.nil? || chain.entries.count(entry) > 1
95
+
96
+ found = retro.entries.index(entry)
97
+ found && (found + 1)
98
+ end
99
+
100
+ # The first definer and, while that definer is `possible`, the next; a nil joins the set when nothing
101
+ # follows (the absent candidate). A cut chain, and a chain on which an external ancestor ahead of the
102
+ # candidate (or, with no candidate, the implicit `Object`) may define the name, is a decline, answered by
103
+ # nil instead of a set.
104
+ def candidates(scope, chain, method_name, from, answer_in)
105
+ hits = []
106
+ position = from
107
+ loop do
108
+ hit = answer_in.call(chain, position)
109
+ hit = past_fold_copies(scope, chain, method_name, hit, answer_in) if chain.side == :singleton
110
+ return nil if hit.equal?(STRAY)
111
+
112
+ raise ArgumentError, "answer function went backwards" if hit && hit.index < position
113
+ return nil if hit.nil? && chain.truncated?
114
+
115
+ hits << hit
116
+ return decided(scope, chain, method_name, from, hits) if hit.nil? || !possible?(scope, hit, method_name)
117
+
118
+ position = hit.index + 1
119
+ end
120
+ end
121
+
122
+ # A class entry answering through a copy the extends fold made is asked past
123
+ # (`SingletonHookDecline.copy_kind`), so the module that wrote the `def` answers at its own position; a copy
124
+ # from a module the chain does not hold on that level is {STRAY}, a decline.
125
+ def past_fold_copies(scope, chain, method_name, hit, answer_in)
126
+ while hit
127
+ case SingletonHookDecline.copy_kind(scope, chain, method_name, hit)
128
+ when :copy then hit = answer_in.call(chain, hit.index + 1)
129
+ when :stray then return STRAY
130
+ else return hit
131
+ end
132
+ end
133
+ hit
134
+ end
135
+
136
+ def decided(scope, chain, method_name, from, hits)
137
+ external_may_answer?(scope, chain, method_name, from, hits.last) ? nil : hits
138
+ end
139
+
140
+ # An external entry ahead of the last candidate that RBS does not know, or knows and declares the name in,
141
+ # may be the definer Ruby calls; with no candidate, so may the implicit `Object` (`Kernel`, for the object
142
+ # that every class is). Both decline. Each tested external files the negative class edge on its spelling.
143
+ def external_may_answer?(scope, chain, method_name, from, last)
144
+ stop = last.nil? ? chain.entries.size : last.index
145
+ recording = Analysis::DependencyRecorder.active?
146
+ (from...stop).any? do |index|
147
+ entry = chain.entries[index]
148
+ next false unless entry.external?
149
+
150
+ Analysis::DependencyRecorder.read_missing(:class, entry.raw.to_s.split("::").last) if recording
151
+ !external_lacks?(scope, entry.candidates, entry.side, method_name)
152
+ end || (last.nil? && implicit_object_answers?(scope, chain, method_name))
153
+ end
154
+
155
+ # The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
156
+ # instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side.
157
+ def implicit_object_answers?(scope, chain, method_name)
158
+ !external_lacks?(scope, ["Object"], chain.side, method_name)
159
+ end
160
+
161
+ # True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
162
+ # external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
163
+ # extended module (`:instance`) and every instance-side entry for an instance method.
164
+ def external_lacks?(scope, candidates, side, method_name)
165
+ return Scope::ResolutionChain::Relevance.external_lacks?(scope, candidates, method_name) if side == :instance
166
+
167
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
168
+ return false if known.nil?
169
+
170
+ Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
171
+ rescue StandardError
172
+ false
173
+ end
174
+
175
+ # What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
176
+ def outcomes(hits)
177
+ return NONE if hits.nil?
178
+
179
+ hits.map { |hit| hit.nil? ? NONE : hit.answer }
180
+ end
181
+
182
+ def collapse(hits)
183
+ first = hits.first
184
+ return ABSENT if hits.all?(&:nil?)
185
+ return UNKNOWN if hits.any?(&:nil?) || hits.any? { |hit| hit.answer != first.answer }
186
+
187
+ Known.new(first.answer, first.owner)
188
+ end
189
+
190
+ # A definer that rests on a fact the walk could not prove executes: the member's `possible` sibling, or a
191
+ # contested slot of one of the single-valued members.
192
+ def possible?(scope, hit, method_name)
193
+ discovery = scope.discovery
194
+ owner = hit.owner
195
+ kind = hit.side == :singleton ? :singleton : :instance
196
+ nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
197
+ discovery.possible_method?(owner, method_name, kind) ||
198
+ discovery.contested?(nodes, [owner, method_name.to_sym]) ||
199
+ discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym]) ||
200
+ discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
201
+ end
202
+
203
+ def default_answer(scope, method_name, question)
204
+ case question
205
+ when :definer then definer_answer(scope, method_name)
206
+ when :visibility then visibility_answer(scope, method_name)
207
+ else raise ArgumentError, "question #{question.inspect} needs an answer function"
208
+ end
209
+ end
210
+
211
+ # The first project entry with a `def` of the name, as `[def node, owner]`.
212
+ def definer_answer(scope, method_name)
213
+ lambda do |chain, from|
214
+ first_hit(chain, scope, from) do |entry|
215
+ node = if entry.side == :singleton
216
+ scope.singleton_def_for(entry.name,
217
+ method_name)
218
+ else
219
+ scope.user_def_for(entry.name,
220
+ method_name)
221
+ end
222
+ [node, entry.name] if node
223
+ end
224
+ end
225
+ end
226
+
227
+ # The first project entry that records the name (a discovered method, an instance `def` the def table
228
+ # holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
229
+ # none was stated).
230
+ def visibility_answer(scope, method_name)
231
+ lambda do |chain, from|
232
+ first_hit(chain, scope, from) do |entry|
233
+ kind = entry.side == :singleton ? :singleton : :instance
234
+ next unless scope.discovered_method?(entry.name, method_name, kind) ||
235
+ (kind == :instance && (scope.user_def_for(entry.name, method_name) ||
236
+ scope.discovered_method_visibility(entry.name, method_name)))
237
+
238
+ scope.discovered_method_visibility(entry.name, method_name) || :public
239
+ end
240
+ end
241
+ end
242
+
243
+ def first_hit(chain, scope, from)
244
+ index = from - 1
245
+ found = nil
246
+ chain.search(scope, from) do |entry|
247
+ index += 1
248
+ next if entry.external?
249
+
250
+ answer = yield(entry)
251
+ next if answer.nil?
252
+
253
+ found = Hit.new(answer, entry.name, index, entry.side)
254
+ end
255
+ found
256
+ end
257
+
258
+ private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
259
+ :external_may_answer?, :implicit_object_answers?, :external_lacks?,
260
+ :outcomes, :collapse, :possible?,
261
+ :default_answer, :definer_answer, :visibility_answer, :first_hit
262
+ end
263
+ end
264
+ end
265
+
266
+ require_relative "singleton_hook_decline"
@@ -45,7 +45,7 @@ module Rigor
45
45
  widened = widen_through_path(current_scope.local(read.name), steps, call_node.name, arg_types)
46
46
  return current_scope if widened.nil?
47
47
 
48
- current_scope.with_local(read.name, widened)
48
+ current_scope.with_mutated_local(read.name, widened)
49
49
  end
50
50
 
51
51
  # True when `receiver` is an element read whose target element is a literal-shape carrier —
@@ -62,7 +62,12 @@ module Rigor
62
62
  # `[<local read>, [step, …]]` for a chain of element reads rooted at a local variable, or
63
63
  # `nil` when the expression is anything else. A step is an Integer position (negative counts
64
64
  # from the end, as Ruby's own indexing does) or `:all` when the index is not a literal.
65
+ #
66
+ # Every mutation site's receiver is asked, and nearly every one is a plain variable read, so a
67
+ # receiver that is no call answers before the step list is allocated.
65
68
  def element_read_path(node)
69
+ return nil unless node.is_a?(Prism::CallNode)
70
+
66
71
  steps = []
67
72
  cursor = node
68
73
  while cursor.is_a?(Prism::CallNode)
@@ -118,22 +123,29 @@ module Rigor
118
123
  end
119
124
 
120
125
  # Rebuilds `type` with the element at `steps` widened against `method_name`, or `nil` when
121
- # nothing along the path widened.
122
- def widen_through_path(type, steps, method_name, arg_types)
123
- return MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types) if steps.empty?
126
+ # nothing along the path widened. A block, when given, replaces the straight-line widening of the
127
+ # element itself: it receives the element's type and answers the widened one, or `nil` to decline.
128
+ # {UnknownStoreWidening} passes one so a site on an element takes the same unknown-store widening a
129
+ # site on a variable does.
130
+ def widen_through_path(type, steps, method_name, arg_types, &element_widening)
131
+ if steps.empty?
132
+ return element_widening.call(type) if element_widening
133
+
134
+ return MutationWidening.widen_for_mutator(type, method_name, arg_types: arg_types)
135
+ end
124
136
 
125
137
  case type
126
- when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types)
127
- when Type::Union then widen_union_path(type, steps, method_name, arg_types)
138
+ when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types, &element_widening)
139
+ when Type::Union then widen_union_path(type, steps, method_name, arg_types, &element_widening)
128
140
  end
129
141
  end
130
142
 
131
- def widen_tuple_path(tuple, steps, method_name, arg_types)
143
+ def widen_tuple_path(tuple, steps, method_name, arg_types, &)
132
144
  elements = tuple.elements.dup
133
145
  rest = steps.drop(1)
134
146
  widened_any = false
135
147
  tuple_positions(tuple, steps.first).each do |position|
136
- inner = widen_through_path(elements[position], rest, method_name, arg_types)
148
+ inner = widen_through_path(elements[position], rest, method_name, arg_types, &)
137
149
  next if inner.nil?
138
150
 
139
151
  elements[position] = inner
@@ -142,10 +154,10 @@ module Rigor
142
154
  widened_any ? Type::Tuple.new(elements) : nil
143
155
  end
144
156
 
145
- def widen_union_path(union, steps, method_name, arg_types)
157
+ def widen_union_path(union, steps, method_name, arg_types, &)
146
158
  widened_any = false
147
159
  members = union.members.map do |member|
148
- widened = widen_through_path(member, steps, method_name, arg_types)
160
+ widened = widen_through_path(member, steps, method_name, arg_types, &)
149
161
  next member if widened.nil?
150
162
 
151
163
  widened_any = true
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../reflection"
6
+ require_relative "../source/node_children"
7
+ require_relative "../type"
8
+ require_relative "block_call_timing"
9
+
10
+ module Rigor
11
+ module Inference
12
+ # Issue #1360 — `$!`, the exception being rescued, and `$@`, its backtrace. Ruby does not keep them in the method
13
+ # frame's special-variable slot, as it keeps `$~` and `$_`: a read of `$!` walks the running execution context's
14
+ # frames to the nearest rescue frame. So:
15
+ #
16
+ # - inside a `rescue` clause, and inside a rescue modifier's fallback, `$!` is the exception that clause rescued,
17
+ # in every block the clause runs and every method it calls;
18
+ # - once the `begin` or the modifier exits, however it exits, `$!` is what it was before it started: `nil` at the
19
+ # top of a method called from nowhere special, the outer exception after a nested `begin` in a rescue clause;
20
+ # - an `else` clause and the body read that earlier value too, and an `ensure` clause reads it when the body or a
21
+ # rescue clause finished, but the exception in flight when one raised.
22
+ #
23
+ # A method body cannot know what its caller is rescuing, so it starts unbound; a thread's or fiber's root block
24
+ # runs in an execution context of its own, where `$!` is `nil` ({FreshFrameBlocks.entry}); and a closure's body
25
+ # runs whenever it is called, usually after the clause it is written in exited ({FreshFrameBlocks.closure_entry}).
26
+ module ErrorInfo
27
+ ERROR_INFO = :$!
28
+ BACKTRACE = :$@
29
+ NAMES = [ERROR_INFO, BACKTRACE].freeze
30
+ # The globals a rescue modifier's fallback reads differently from the modifier's entry ({.modifier_entry}).
31
+ MODIFIER_NAMES = [ERROR_INFO, BACKTRACE, :$?].freeze
32
+ EXCEPTION_ORDERINGS = Set[:equal, :subclass].freeze
33
+ DEFINERS = Set[:define_method, :define_singleton_method].freeze
34
+ # The class guards a clause may put on `$!` ({.guarded?}).
35
+ GUARDS = Set[:is_a?, :kind_of?, :instance_of?, :respond_to?].freeze
36
+ private_constant :ERROR_INFO, :BACKTRACE, :NAMES, :MODIFIER_NAMES, :EXCEPTION_ORDERINGS, :GUARDS, :DEFINERS
37
+
38
+ module_function
39
+
40
+ # The scope a `rescue` clause, or a rescue modifier's fallback, runs from: `$!` bound to the exception it rescued
41
+ # and `$@` to that exception's backtrace, with `$?` unbound. `exception_type` is what `rescue … => e` binds `e` to
42
+ # (`StatementEvaluator#rescue_exception_type`): the union of the named classes' instances, `StandardError` for a
43
+ # bare `rescue`. A member that is not a class RBS or the program places below `Exception` reads `Dynamic[top]`
44
+ # instead: a rescue list may name a module with its own `===` (`rescue NetworkErrors`), which matches exceptions
45
+ # that are not instances of it, and a class the analyzer cannot place may be such an object too. So does a class
46
+ # that it or a project ancestor gives its own singleton `===` (`class Matchy < StandardError; def self.===(o) =
47
+ # true`).
48
+ #
49
+ # `body` is the clause's statements or the fallback, and `reference` the name of the local a `rescue … => e`
50
+ # clause binds. When the body guards `$!`, or that local, which is the same object, by its class ({.guarded?}),
51
+ # the clause reads `$!` and `$@` unbound, as it did before issue #1360: `$!.key if e.is_a?(KeyError)` would report
52
+ # the bound `StandardError` against the guard (ADR-117, point 3), since narrowing `e` does not narrow `$!`. #1429
53
+ # made a class guard on `$!` itself narrow it; #1447 narrows this decline to the guards that still do not.
54
+ #
55
+ # `$@` calls the exception's `backtrace`, which is an `Array[String]` for a raised exception. It is left unbound
56
+ # in a program that defines a method named `backtrace` anywhere, which may return anything, and in a clause
57
+ # whose body calls `set_backtrace`, which may set it to nil.
58
+ #
59
+ # `$?` is unbound because the exception may have been raised while a subprocess waited: a backtick, `%x` or
60
+ # `system` sets `$?` to nil before it runs the child, so a `Timeout::Error`, an `Interrupt` or a `Thread#raise`
61
+ # there leaves it nil.
62
+ def rescue_entry(scope, exception_type, body = nil, reference = nil)
63
+ entered = scope.forget_error_info.forget_last_status
64
+ # Narrowed to the guards #1429 does not narrow in #1447.
65
+ return entered if guarded?(body, reference)
66
+
67
+ entered = entered.with_global(ERROR_INFO, rescued_type(exception_type, scope))
68
+ return entered if calls_set_backtrace?(body) || BlockCallTiming.project_defines_anywhere?(:backtrace, scope)
69
+
70
+ entered.with_global(BACKTRACE, backtrace_type)
71
+ end
72
+
73
+ # The scope a rescue modifier's fallback (`expr rescue fallback`) runs from, which rescues a `StandardError`.
74
+ def modifier_entry(scope, fallback = nil)
75
+ rescue_entry(scope, Type::Combinator.nominal_of("StandardError"), fallback)
76
+ end
77
+
78
+ # The class guards on `$!` that {.rescue_entry} declines on, anywhere in `node`: `$!.is_a?`, `kind_of?`,
79
+ # `instance_of?` or `respond_to?`, a `===` whose argument is `$!` (`KeyError === $!`), and `case $!`, and the
80
+ # same on a read of the local named `reference`.
81
+ def guarded?(node, reference = nil)
82
+ return false unless node.is_a?(Prism::Node)
83
+ return true if guard?(node, reference)
84
+
85
+ found = false
86
+ node.rigor_each_child { |child| found ||= guarded?(child, reference) }
87
+ found
88
+ end
89
+
90
+ def guard?(node, reference)
91
+ case node
92
+ when Prism::CallNode
93
+ (GUARDS.include?(node.name) && error_info_read?(node.receiver, reference)) ||
94
+ (node.name == :=== && error_info_read?(node.arguments&.arguments&.first, reference))
95
+ when Prism::CaseNode, Prism::CaseMatchNode then error_info_read?(node.predicate, reference)
96
+ else false
97
+ end
98
+ end
99
+
100
+ def error_info_read?(node, reference)
101
+ case node
102
+ when Prism::GlobalVariableReadNode then node.name == ERROR_INFO
103
+ when Prism::LocalVariableReadNode then !reference.nil? && node.name == reference
104
+ else false
105
+ end
106
+ end
107
+
108
+ # True when `node` calls `set_backtrace` anywhere, on any receiver: `$!.set_backtrace(nil)` makes `$@` nil.
109
+ def calls_set_backtrace?(node)
110
+ return false unless node.is_a?(Prism::Node)
111
+ return true if node.is_a?(Prism::CallNode) && node.name == :set_backtrace
112
+
113
+ found = false
114
+ node.rigor_each_child { |child| found ||= calls_set_backtrace?(child) }
115
+ found
116
+ end
117
+ private_class_method :guard?, :error_info_read?, :calls_set_backtrace?
118
+
119
+ # True when `node` reads `$!`, `$@` or `$?` anywhere in it: the gate on typing a rescue modifier's fallback
120
+ # under {.modifier_entry}, which most fallbacks (`rescue nil`) never need.
121
+ def read_in?(node)
122
+ return false unless node.is_a?(Prism::Node)
123
+ return MODIFIER_NAMES.include?(node.name) if node.is_a?(Prism::GlobalVariableReadNode)
124
+
125
+ found = false
126
+ node.rigor_each_child { |child| found ||= read_in?(child) }
127
+ found
128
+ end
129
+
130
+ # `scope` with `$!` and `$@` as `entry` binds them, and unbound where `entry` leaves them unbound: the scope past
131
+ # a `begin` or a rescue modifier that started from `entry`, whichever way it left.
132
+ def restore(scope, entry)
133
+ return scope if NAMES.all? { |name| scope.global(name).equal?(entry.global(name)) }
134
+
135
+ NAMES.reduce(scope.forget_error_info) do |acc, name|
136
+ bound = entry.global(name)
137
+ bound ? acc.with_global(name, bound) : acc
138
+ end
139
+ end
140
+
141
+ def rescued_type(exception_type, scope)
142
+ members = exception_type.is_a?(Type::Union) ? exception_type.members : [exception_type]
143
+ return exception_type if members.all? { |member| exception_instance?(member, scope) }
144
+
145
+ Type::Combinator.union(
146
+ *members.map { |member| exception_instance?(member, scope) ? member : Type::Combinator.untyped }
147
+ )
148
+ end
149
+
150
+ # True when `type` is an instance of a class below `Exception`: one RBS places there, or a project class whose
151
+ # superclass chain reaches one (`class MyError < StandardError`), which the RBS environment does not know.
152
+ def exception_instance?(type, scope)
153
+ return false unless type.is_a?(Type::Nominal)
154
+
155
+ class_name = type.class_name
156
+ return false if own_case_equality?(class_name, scope)
157
+ return true if exception_class?(class_name, scope)
158
+ return false unless scope.known_user_class?(class_name)
159
+
160
+ scope.external_ancestor_name_candidates(class_name, mixins: false).any? do |candidates|
161
+ known = candidates.find { |candidate| Reflection.rbs_class_known?(candidate, scope: scope) }
162
+ !known.nil? && exception_class?(known, scope)
163
+ end
164
+ rescue StandardError
165
+ false
166
+ end
167
+
168
+ # True when the program gives `class_name`, or a project ancestor of it, a singleton `===`, which `rescue` calls
169
+ # to match and which may accept an exception that is not an instance of it: a `def self.===` or `class << self`
170
+ # `def ===`, or, anywhere in the file being read, a `define_method` or `define_singleton_method` naming `===`
171
+ # ({.defines_case_equality?}), which the discovery tables do not place on a class, so it counts for every class.
172
+ def own_case_equality?(class_name, scope)
173
+ scope.discovery.defines_case_equality ||
174
+ !scope.singleton_def_through_ancestors(class_name, :===).first.nil?
175
+ end
176
+
177
+ # True when `node` is a `define_method` or `define_singleton_method` call whose literal name is `===`.
178
+ # `Inference::ScopeIndexer` records whether a file holds one ({Scope::DiscoveryIndex#defines_case_equality}).
179
+ def defines_case_equality?(node)
180
+ return false unless node.is_a?(Prism::CallNode) && DEFINERS.include?(node.name)
181
+
182
+ name = node.arguments&.arguments&.first
183
+ (name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)) && name.unescaped == "==="
184
+ end
185
+
186
+ def exception_class?(class_name, scope)
187
+ EXCEPTION_ORDERINGS.include?(scope.environment.class_ordering(class_name, "Exception"))
188
+ end
189
+
190
+ def backtrace_type
191
+ Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.nominal_of("String")])
192
+ end
193
+ private_class_method :rescued_type, :exception_instance?, :own_case_equality?, :exception_class?, :backtrace_type
194
+ end
195
+ end
196
+ end