rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -0,0 +1,392 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../type"
7
+ require_relative "block_call_timing"
8
+ require_relative "fresh_frame_blocks"
9
+ require_relative "guard_rebinding"
10
+ require_relative "stored_block_call"
11
+ require_relative "match_rebinding/frame"
12
+ require_relative "match_rebinding/operands"
13
+ require_relative "match_rebinding/calls"
14
+ require_relative "match_rebinding/self_calls"
15
+ require_relative "last_line"
16
+
17
+ module Rigor
18
+ module Inference
19
+ # Which code may rebind the regex match globals (`$~` and the `$&` / `` $` `` / `$'` / `$+` / `$1`..`$9` family
20
+ # derived from it) that a scope's narrowing speaks for. Ruby keeps them in the method frame's special-variable
21
+ # slot, and every block and closure made in the method reaches that same slot, so a match a block body runs
22
+ # rebinds the enclosing method's `$~` (issue #1358). A `def`, class or module body runs in a frame of its own,
23
+ # and so does every call into a method defined in Ruby: a match in the callee writes the callee's slot, never
24
+ # its caller's (issue #1364). A thread, fiber or ractor keeps a slot of its own for the root block it runs
25
+ # ({FreshFrameBlocks}, issue #1361), so no scan here counts what that block runs; what it makes can still reach
26
+ # this frame's slot from the creator's thread, so the frame-wide rules keep reading inside it.
27
+ #
28
+ # The block and closure scans are syntactic, resolving only constants and the variables a lookup argument
29
+ # names, through the scope they are given. The code a statement runs outside a block is read call by call
30
+ # ({Calls}, {.value_may_rebind?}; issue #1365): a call the old name table forgot on keeps forgetting unless its
31
+ # literal arguments prove it match-free, and any other forgets only when it is known to match. The scans
32
+ # short-circuit without a `return` out of a child block, which would allocate once per frame it unwinds.
33
+ module MatchRebinding # rubocop:disable Metrics/ModuleLength
34
+ # The block and closure scans read calls on narrower terms than the statement-level rule ({Calls}), because
35
+ # inside a block `[]`, `split` and `index` are overwhelmingly lookups on hashes, arrays and strings whose key
36
+ # the scan cannot type, and counting them dropped the narrowing on correct code
37
+ # (`fields.each { |f| out[f] = row[f] }; $1.upcase`).
38
+ #
39
+ # These rebind `$~` whatever their argument: `sub` / `gsub` / `scan` with a String pattern still set it, and
40
+ # `!~` runs `=~`.
41
+ ALWAYS_MATCHING = Set[:=~, :!~, :match, :sub, :sub!, :gsub, :gsub!, :scan].freeze
42
+ # These rebind it only with a Regexp argument, so they count only when an argument is known to be one
43
+ # ({Operands.regexp_argument?}). `match?` never sets `$~`.
44
+ REGEXP_ARGUMENT = Set[
45
+ :[], :slice, :slice!, :index, :rindex, :partition, :rpartition, :split, :grep, :grep_v,
46
+ :start_with?, :byteindex, :byterindex, :any?, :all?, :none?, :one?
47
+ ].freeze
48
+ # `grep` / `grep_v` rebind the caller's `$~` only in their block form.
49
+ BLOCK_FORM_ONLY = Set[:grep, :grep_v].freeze
50
+ # `&:name` block arguments that rebind `$~`: the method runs on the element in the caller's frame. `===` with
51
+ # an unknown operand may be a Regexp's. Compared as Strings, as {SelfCalls} explains.
52
+ MATCHING_SYMBOL_PROCS = (ALWAYS_MATCHING | Set[:===]).to_set(&:to_s).freeze
53
+ # The broad reading ({.broad_may_match?}) also counts these with any argument that may be a Regexp.
54
+ BROAD_ARGUMENT = (REGEXP_ARGUMENT | Set[:[]=]).freeze
55
+ # The names issue #1364 added to the block scan. The entry of a `tap` / `then` / `yield_self` block reads its
56
+ # body without them ({.block_entry}).
57
+ ADDED_NAMES = Set[:!~, :start_with?, :byteindex, :byterindex, :any?, :all?, :none?, :one?].freeze
58
+ # The calls on the method's own `&block` parameter that run it, which the broad reading counts.
59
+ BLOCK_INVOCATIONS = Set[:call, :yield, :[], :===].freeze
60
+
61
+ # A body that runs in a frame of its own, or never runs (`defined?` evaluates nothing): every walk of what a
62
+ # frame runs stops there, `$_`'s ({LastLine}) included.
63
+ OWN_FRAME_NODES = Set[
64
+ Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefinedNode
65
+ ].freeze
66
+ # A bare regex condition (`if /re/`) matches against `$_`.
67
+ LAST_LINE_MATCHES = Set[Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode].freeze
68
+ GLOBAL_WRITES = Set[
69
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode,
70
+ Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableTargetNode
71
+ ].freeze
72
+ private_constant :ALWAYS_MATCHING, :REGEXP_ARGUMENT, :BLOCK_FORM_ONLY, :MATCHING_SYMBOL_PROCS, :BROAD_ARGUMENT,
73
+ :ADDED_NAMES, :BLOCK_INVOCATIONS,
74
+ :LAST_LINE_MATCHES, :GLOBAL_WRITES
75
+
76
+ module_function
77
+
78
+ # True when running `node` may run a regex match in the frame it runs in, anywhere in `node` but a nested
79
+ # `def`, class or module body: a call {.call_matches?} counts; so does a `when` condition of a `case` with a
80
+ # subject, or an `in` / `=>` pattern value, that may be a Regexp ({Operands}); a bare regex condition; a
81
+ # write to `$~`; or a block argument {.block_argument_may_match?} counts. A block or lambda inside `node`
82
+ # counts, since it runs in the same frame. `scope` resolves constants and the variables a lookup argument
83
+ # names, and names the frame; the answer for a node is kept on that frame while those bindings stay the same.
84
+ def may_match?(node, scope = nil)
85
+ return false unless node.is_a?(Prism::Node)
86
+
87
+ remember(node, scope) { scan(node, scope) }
88
+ end
89
+
90
+ # The answer the block gives for `node`, kept on `scope`'s frame under `kind` ({Frame#memo}) when a body
91
+ # stamped one.
92
+ def remember(node, scope, kind = nil, &)
93
+ frame = scope&.match_frame
94
+ frame.nil? ? yield : frame.memo(node, scope, kind, &)
95
+ end
96
+
97
+ # `base` reads the body without {ADDED_NAMES}.
98
+ def scan(node, scope, base: false)
99
+ return true if matching_node?(node, scope, base: base)
100
+ return false if OWN_FRAME_NODES.include?(node.class)
101
+
102
+ FreshFrameBlocks.any_frame_child?(node, scope) { |child| scan(child, scope, base: base) }
103
+ end
104
+ private_class_method :scan
105
+
106
+ # `broad` reads an unresolved constant as a possible Regexp ({Operands}).
107
+ def matching_node?(node, scope, broad: false, base: false)
108
+ case node
109
+ when Prism::CallNode then call_matches?(node, scope, broad: broad, base: base)
110
+ when Prism::CaseNode then case_matches?(node, scope, broad: broad)
111
+ when Prism::BlockArgumentNode then block_argument_may_match?(node, scope, base: base)
112
+ # `case … in`, `expr in pat` and `expr => pat` run `===` on each value in the pattern.
113
+ when Prism::InNode, Prism::MatchPredicateNode, Prism::MatchRequiredNode
114
+ Operands.pattern_matches?(node.pattern, scope, broad: broad)
115
+ else
116
+ LAST_LINE_MATCHES.include?(node.class) || (GLOBAL_WRITES.include?(node.class) && node.name == :$~)
117
+ end
118
+ end
119
+ private_class_method :matching_node?
120
+
121
+ # A call that rebinds `$~` in the frame it is made in: an {ALWAYS_MATCHING} name, a {REGEXP_ARGUMENT} name
122
+ # with a known Regexp argument, or `===` on a receiver that may be a Regexp — `a === b` is `a`'s method, so
123
+ # `String === re` runs no match.
124
+ def call_matches?(node, scope, broad: false, base: false)
125
+ name = node.name
126
+ return false if base && ADDED_NAMES.include?(name)
127
+ return true if ALWAYS_MATCHING.include?(name)
128
+
129
+ if name == :===
130
+ receiver = node.receiver
131
+ return receiver.nil? || Operands.pattern_value?(receiver, scope, broad: broad)
132
+ end
133
+ arguments = node.arguments&.arguments
134
+ return false unless arguments && REGEXP_ARGUMENT.include?(name)
135
+ return false if BLOCK_FORM_ONLY.include?(name) && node.block.nil?
136
+
137
+ arguments.any? { |argument| Operands.regexp_argument?(argument, scope) }
138
+ end
139
+
140
+ # A `case` with a subject runs `condition === subject` for each `when` condition. One without a subject tests
141
+ # each condition for truth, which runs no `===`; a match inside a condition is scanned like any other code.
142
+ def case_matches?(node, scope, broad: false)
143
+ return false if node.predicate.nil?
144
+
145
+ node.conditions.any? do |clause|
146
+ clause.is_a?(Prism::WhenNode) &&
147
+ clause.conditions.any? { |condition| Operands.pattern_value?(condition, scope, broad: broad) }
148
+ end
149
+ end
150
+ private_class_method :call_matches?, :case_matches?
151
+
152
+ # True when an implicit-self call's arguments may run a match in this frame (issue #1364), outside a block or
153
+ # lambda, which {.value_may_rebind?} and the frame's closure rules answer for: a call the table or
154
+ # {SelfCalls} names on any receiver, unless its syntax proves it cannot match ({Calls.forgets_by_name?}, issue
155
+ # #1365: `log(row[:name])` keeps the narrowing, `log(row[key])` does not); a Symbol or String literal naming
156
+ # one (`inject(:=~)`), which the implicit-self call may run from C; a `yield`; or any other construct the
157
+ # block scan counts, an unresolved constant read as a possible Regexp. An implicit-self call used to forget
158
+ # whatever it called, and this keeps it forgetting wherever it forgot then but on that proof; a call in an
159
+ # argument that is known to match forgets by itself too ({.operands_may_rebind?}).
160
+ def operand_may_match?(node, scope = nil)
161
+ return false unless node.is_a?(Prism::Node)
162
+
163
+ case node
164
+ when Prism::BlockNode, Prism::LambdaNode then return false
165
+ when Prism::CallNode
166
+ return true if Calls.base_named?(node, implicit: true) && Calls.forgets_by_name?(node, scope)
167
+ when Prism::YieldNode then return true
168
+ else
169
+ return true if SelfCalls.method_name_literal?(node) || matching_node?(node, scope, broad: true)
170
+ end
171
+ return false if OWN_FRAME_NODES.include?(node.class)
172
+
173
+ FreshFrameBlocks.any_frame_child?(node, scope) { |child| operand_may_match?(child, scope) }
174
+ end
175
+
176
+ # True when `node`, a frame's body or parameters, hands its match globals to code the analyzer does not trace,
177
+ # so an implicit-self call in the frame keeps forgetting them as every one did before issue #1364: a block or
178
+ # lambda literal whose body may match by the broad reading ({.broad_may_match?}), since the call it is passed
179
+ # to may keep it and run it from any later call; `binding` in any spelling, whose `eval` runs in this frame
180
+ # wherever it is called; or a forward of the method's own block (`&blk` for the method's `&blk`, `&`, `...`),
181
+ # which may be a C-function proc such as `&:=~`. `block_name` is the method's `&block` parameter. The root block
182
+ # of a thread, fiber or ractor ({FreshFrameBlocks.root_block}) is not handed to such code: it runs with a slot
183
+ # of its own. A block, lambda or `binding` inside it is read as anywhere else, since it may be handed back to
184
+ # this frame's thread (`Thread.new { -> { s =~ re } }.value.call` rebinds the creator's `$~`).
185
+ def self_call_fallback?(node, block_name, scope = nil)
186
+ hands_out_slot?(node, block_name, scope) { |body| broad_may_match?(body, scope, block_name) }
187
+ end
188
+
189
+ # The walk under {.self_call_fallback?}, shared with `$_` ({LastLine.fallback?}): the block answers whether a
190
+ # block or lambda body may rebind the special it asks about.
191
+ def hands_out_slot?(node, block_name, scope, &)
192
+ return false unless node.is_a?(Prism::Node)
193
+ return true if handoff_node?(node, block_name, &)
194
+ return false if OWN_FRAME_NODES.include?(node.class)
195
+
196
+ FreshFrameBlocks.any_frame_child?(node, scope, into_root: true) do |child|
197
+ hands_out_slot?(child, block_name, scope, &)
198
+ end
199
+ end
200
+
201
+ def handoff_node?(node, block_name)
202
+ case node
203
+ when Prism::BlockNode, Prism::LambdaNode then yield(node.body)
204
+ when Prism::CallNode then node.name == :binding || SelfCalls.sends_binding?(node)
205
+ when Prism::ForwardingArgumentsNode then true
206
+ when Prism::BlockArgumentNode
207
+ expression = node.expression
208
+ expression.nil? || (expression.is_a?(Prism::LocalVariableReadNode) && expression.name == block_name)
209
+ else false
210
+ end
211
+ end
212
+
213
+ # {.may_match?} on broad terms, where over-counting costs no more than an implicit-self call forgetting as it
214
+ # did before issue #1364: a {BROAD_ARGUMENT} name counts with any argument that may be a Regexp — a block
215
+ # parameter, a method's return value, a constant that does not resolve (#1373) — and so does a call {SelfCalls}
216
+ # reads by name, a literal naming one, a `yield`, or a call that runs the method's own `&block` (`block_name`),
217
+ # either of which may run a C-function proc such as `&:=~` in this frame.
218
+ def broad_may_match?(node, scope = nil, block_name = nil)
219
+ return false unless node.is_a?(Prism::Node)
220
+ return true if broad_matching_node?(node, scope, block_name)
221
+ return false if OWN_FRAME_NODES.include?(node.class)
222
+
223
+ FreshFrameBlocks.any_frame_child?(node, scope) { |child| broad_may_match?(child, scope, block_name) }
224
+ end
225
+
226
+ def broad_matching_node?(node, scope, block_name)
227
+ return true if node.is_a?(Prism::YieldNode)
228
+ unless node.is_a?(Prism::CallNode)
229
+ return matching_node?(node, scope, broad: true) || SelfCalls.method_name_literal?(node)
230
+ end
231
+ return true if call_matches?(node, scope, broad: true) || SelfCalls.named_match?(node)
232
+ return true if own_block_call?(node, block_name)
233
+
234
+ arguments = node.arguments&.arguments
235
+ !arguments.nil? && BROAD_ARGUMENT.include?(node.name) &&
236
+ arguments.any? { |argument| Operands.pattern_value?(argument, scope, broad: true) }
237
+ end
238
+
239
+ def own_block_call?(call_node, block_name)
240
+ receiver = call_node.receiver
241
+ !block_name.nil? && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == block_name &&
242
+ BLOCK_INVOCATIONS.include?(call_node.name)
243
+ end
244
+ private_class_method :handoff_node?, :broad_matching_node?, :own_block_call?
245
+
246
+ # True when the block `call_node` passes may rebind the frame's match globals while the call runs: a block
247
+ # literal whose body {.may_match?}, or a block argument that {.block_argument_may_match?}. The root block of a
248
+ # thread, fiber or ractor never does ({FreshFrameBlocks.root_call?}); a root `&expr` argument's expression runs
249
+ # here first, as an operand does ({.value_may_rebind?}).
250
+ def block_may_match?(call_node, scope = nil)
251
+ block = call_node.block
252
+ if FreshFrameBlocks.root_call?(call_node, scope)
253
+ return block.is_a?(Prism::BlockArgumentNode) && value_may_rebind?(block.expression, scope)
254
+ end
255
+
256
+ case block
257
+ when Prism::BlockNode then may_match?(block.body, scope)
258
+ when Prism::BlockArgumentNode then block_argument_may_match?(block, scope)
259
+ else false
260
+ end
261
+ end
262
+
263
+ # True when a `&expr` block argument may pass a proc made in this frame. Not an anonymous `&`, nor the method's
264
+ # own `&block` parameter while the body never rebinds or shadows it ({Frame#forwarded_block?}): either
265
+ # forwards the block the caller made, in the caller's frame. Not a `&:name` whose method cannot match
266
+ # ({MATCHING_SYMBOL_PROCS}).
267
+ def block_argument_may_match?(block_argument, scope, base: false)
268
+ expression = block_argument.expression
269
+ case expression
270
+ when nil then false
271
+ when Prism::SymbolNode
272
+ name = expression.unescaped
273
+ MATCHING_SYMBOL_PROCS.include?(name) && !(base && name == "!~")
274
+ when Prism::LocalVariableReadNode
275
+ frame = scope&.match_frame
276
+ frame.nil? || !frame.forwarded_block?(expression.name)
277
+ else true
278
+ end
279
+ end
280
+
281
+ # True when `program`, a String of code an eval runs in this frame, may match, on the block scan's terms: its
282
+ # statements run here, and a `def` or class in it runs in a frame of its own. The caller keeps the answer.
283
+ def program_may_match?(program, scope) = scan(program, scope)
284
+
285
+ # True when running a call's receiver chain and arguments, which Ruby does before the method, may rebind the
286
+ # frame's match globals ({.value_may_rebind?}), kept on the frame as {Frame#memo} keeps a scan.
287
+ def operands_may_rebind?(call_node, scope)
288
+ remember(call_node, scope, :operands) do
289
+ value_may_rebind?(call_node.receiver, scope) || value_may_rebind?(call_node.arguments, scope)
290
+ end
291
+ end
292
+
293
+ # True when evaluating `node` for its value — a call's receiver chain or arguments, an array, hash or
294
+ # interpolation literal, a `rescue` modifier, a `super` or `yield` — may rebind the frame's match globals
295
+ # (issue #1365). No call there forgot before, so each forgets only when it is known to match
296
+ # ({Calls.rebinds?}): `[u.index(/(q)/)]` does, `out << row[key]` and `x = [h[k], 1]` do not. So does an index
297
+ # write whose index is known to be a Regexp (`s[re] ||= v`), a block literal on a call there whose body
298
+ # {.may_match?}, a block argument {.block_argument_may_match?} counts, or any other construct the block scan
299
+ # counts (a `when` or `in` value that may be a Regexp, a bare regex condition, a write to `$~`). An
300
+ # implicit-self call there is read the same way: the frame-wide fallback of {Calls.statement_rebinds?} stays with
301
+ # statement-position calls, as before, where an operand's `value.upcase` would otherwise forget at every
302
+ # attribute read. A lambda literal does not run where it is written ({.matching_closure?} answers for it),
303
+ # and a `def`, class or module body or a `defined?` operand does not run in this frame.
304
+ def value_may_rebind?(node, scope)
305
+ return false unless node.is_a?(Prism::Node)
306
+
307
+ case node
308
+ when Prism::BlockNode then return may_match?(node.body, scope)
309
+ when Prism::LambdaNode then return false
310
+ when Prism::CallNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode, Prism::IndexOperatorWriteNode
311
+ return true if Calls.rebinds?(node, scope)
312
+ else return true if matching_node?(node, scope)
313
+ end
314
+ return false if OWN_FRAME_NODES.include?(node.class)
315
+
316
+ FreshFrameBlocks.any_frame_child?(node, scope) { |child| value_may_rebind?(child, scope) }
317
+ end
318
+
319
+ # True when `node`, a frame's body or parameters, makes a closure that may rebind the frame's match globals
320
+ # whenever it is invoked: a `->` literal, or the block of a call that keeps it to run later
321
+ # ({StoredBlockCall}: `lambda`, `proc`, `Proc.new`, `define_method`, …), whose body {.may_match?}.
322
+ # Invocations are not traced — the closure can be called through any later call, or run by a method it was
323
+ # handed to — so {Frame} answers for the whole frame. The root block of a thread or fiber is kept too, but it
324
+ # only ever runs as that thread's or fiber's root, with a slot of its own ({FreshFrameBlocks}); a closure made
325
+ # inside it still counts, since it may be handed back to this frame's thread.
326
+ def matching_closure?(node, scope = nil) = makes_closure?(node, scope) { |body| may_match?(body, scope) }
327
+
328
+ # The walk under {.matching_closure?}, shared with `$_` ({LastLine.closure?}): the block answers whether a
329
+ # closure body may rebind the special it asks about.
330
+ def makes_closure?(node, scope, &)
331
+ return false unless node.is_a?(Prism::Node)
332
+ return false if OWN_FRAME_NODES.include?(node.class)
333
+ return true if node.is_a?(Prism::LambdaNode) && yield(node.body)
334
+ return true if node.is_a?(Prism::CallNode) && stored_block_rebinds?(node, scope, &)
335
+
336
+ found = false
337
+ node.rigor_each_child { |child| found ||= makes_closure?(child, scope, &) }
338
+ found
339
+ end
340
+
341
+ def stored_block_rebinds?(call_node, scope)
342
+ block = call_node.block
343
+ block.is_a?(Prism::BlockNode) && StoredBlockCall.stores_block?(call_node) &&
344
+ !FreshFrameBlocks.root_call?(call_node, scope) && yield(block.body)
345
+ end
346
+ private_class_method :stored_block_rebinds?
347
+
348
+ # The scope a block or lambda body enters with: `scope` with its match globals forgotten when the body may
349
+ # match, or when the frame makes a closure that may. The body can run on a later iteration, after an earlier
350
+ # one — or a call to that closure — rebound them, so no iteration may read the narrowing the call site holds.
351
+ # A body with neither keeps it: blocks share the frame, so `s =~ /(\d+)/; items.map { $1 }` reads the
352
+ # guard's `$1`. The block of a call named `tap`, `then` or `yield_self` ({BlockCallTiming}) is read as the scan
353
+ # read every block before issue #1364, without {ADDED_NAMES}, so its entry is what it was: the name alone
354
+ # cannot show the block runs once (a user `then` may keep it, and a loop runs the call again, #1375). With
355
+ # the owning `call_node`, the entry also forgets when the call's receiver chain or arguments may rebind them
356
+ # ({.operands_may_rebind?}, issue #1365): Ruby runs those before the method yields, so
357
+ # `[u.index(/(q)/)].map { $1 }` reads the rebound `$1`. Every block-entry pass enters through here:
358
+ # {StatementEvaluator#build_block_entry_scope} and the block-return and `break` passes in {ExpressionTyper}
359
+ # pass the call. The per-element fold and the captured-local fixpoint pass none; they run under
360
+ # `ExpressionTyper#rebound_operand_typer`, whose scope has already forgotten the globals when the operands
361
+ # may rebind them, so no pass disagrees. Neither runs for the block of a call {FreshFrameBlocks} names, which is
362
+ # no iterator's; that block enters as {FreshFrameBlocks.entry} gives (issue #1361): a thread's, fiber's or
363
+ # ractor's root block with the globals unbound, since it reads a slot of its own, and a `define_method` body
364
+ # with a narrowed global untyped, since it reads the definer's slot whenever the method is called. `$_`, which
365
+ # shares the slot, enters on the same terms ({LastLine.block_entry}, issue #1359). A closure's body, which runs
366
+ # whenever it is called, enters with `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}, issue #1360).
367
+ #
368
+ # Issue #1429 — the entry also restores a guard's narrowing of a global or constant where the body may run after
369
+ # code that rebinds it ({GuardRebinding.block_entry}).
370
+ def block_entry(scope, block_node, call_node = nil)
371
+ scope = GuardRebinding.block_entry(scope, block_node, call_node)
372
+ return FreshFrameBlocks.entry(scope, call_node) if FreshFrameBlocks.fresh_entry?(call_node, scope)
373
+
374
+ scope = FreshFrameBlocks.closure_entry(scope, block_node, call_node)
375
+ scope = LastLine.block_entry(scope, block_node, call_node)
376
+ return scope unless scope.match_globals_bound?
377
+ return scope unless scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
378
+ (call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
379
+
380
+ scope.forget_match_globals
381
+ end
382
+
383
+ def entry_may_match?(body, scope, call_node)
384
+ return may_match?(body, scope) unless call_node.is_a?(Prism::CallNode) &&
385
+ BlockCallTiming.candidate_name?(call_node.name)
386
+
387
+ body.is_a?(Prism::Node) && scan(body, scope, base: true)
388
+ end
389
+ private_class_method :entry_may_match?
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module MethodDispatcher
6
+ module OverloadSelector
7
+ # Canonical RBS-core aliases shipped by `core/builtin.rbs` whose body is `<Nominal> | _DuckType`.
8
+ # Matching an overload against an Integer literal should pick the `(int) -> Array[Elem]` body over
9
+ # the `(string) -> String` body because Integer satisfies `int`'s strict arm and not `string`'s.
10
+ # The translator collapses both aliases to `Dynamic[Top]` (interfaces are not structurally matched
11
+ # yet), so a dedicated pass 1.5 between strict and gradual consults this map to pick the alias
12
+ # whose strict arm matches.
13
+ #
14
+ # Symbol keys are the alias names as they appear under `RBS::Types::Alias#name.to_s` (the `name` is
15
+ # a `TypeName` whose `to_s` includes the `::` prefix). Values are an Array of class names whose
16
+ # Nominal[..] form is the alias's strict-arm matcher.
17
+ #
18
+ # `range[T] = Range[T] | _Range[T]` is generic, unlike the others, but its strict arm is still a single
19
+ # nominal and the args are irrelevant to this pass. rbs 4.1 rewrote `Array#[]`'s slicing overload from
20
+ # `(::Range[::Integer?])` to `(range[int])`; without the entry both it and the `(int) -> E` overload look
21
+ # alias-typed, so `a[1..2]` resolved to the element type.
22
+ ALIAS_STRICT_NOMINALS = Ractor.make_shareable({
23
+ "::int" => ["Integer"],
24
+ "::string" => ["String"],
25
+ "::interned" => %w[Symbol String],
26
+ "::io" => ["IO"],
27
+ "::encoding" => %w[Encoding String],
28
+ "::path" => ["String"],
29
+ "::boolean" => %w[TrueClass FalseClass],
30
+ "::range" => ["Range"]
31
+ })
32
+ private_constant :ALIAS_STRICT_NOMINALS
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../type"
4
+ require_relative "iterator_dispatch"
4
5
 
5
6
  module Rigor
6
7
  module Inference
@@ -15,13 +16,20 @@ module Rigor
15
16
  # The methods covered fall in two families:
16
17
  #
17
18
  # - **Filter-shaped** (`select` / `filter` / `reject` / `take_while` / `drop_while`): the block's
18
- # truthiness selects the all-or-nothing endpoints — either the receiver's full shape (when every
19
- # element is kept) or the empty-tuple carrier (when every element is dropped).
20
- # - **Predicate-shaped** (`all?` / `any?` / `none?`): the block's truthiness combined with the
21
- # receiver's emptiness collapses the call to a `Constant[bool]` in the cases where Ruby's actual
22
- # semantics make it unconditional. Non-empty + truthy `any?` is `true`; non-empty + falsey `all?` is
23
- # `false`; the empty-receiver "vacuous" answers (`[].all? { false } == true`, `[].any? { true } ==
24
- # false`, `[].none? { true } == true`) are likewise honoured.
19
+ # truthiness selects the all-or-nothing endpoints — either every element kept or every element
20
+ # dropped — and each endpoint is a value of the class the method returns. `Hash#select` / `#filter`
21
+ # / `#reject` keep the Hash receiver or drop to the empty HashShape; every other combination is
22
+ # Enumerable's and returns an Array, so it drops to the empty-tuple carrier and keeps the receiver
23
+ # only when that receiver is itself an Array (otherwise the all-kept side declines).
24
+ # - **Predicate-shaped** (`all?` / `any?` / `none?`, without a pattern argument): the block's
25
+ # truthiness combined with the receiver's emptiness collapses the call to a `Constant[bool]` in the
26
+ # cases where Ruby's actual semantics make it unconditional. Non-empty + truthy `any?` is `true`;
27
+ # non-empty + falsey `all?` is `false`; the empty-receiver "vacuous" answers (`[].all? { false } ==
28
+ # true`, `[].any? { true } == false`, `[].none? { true } == true`) are likewise honoured.
29
+ #
30
+ # One family reads no block truthiness at all: **edge-selecting** `min_by` / `max_by` without a count
31
+ # answer `nil` only for an empty receiver, so on a receiver whose non-emptiness is a static fact they
32
+ # answer the element type without the `nil` the RBS `Elem?` return adds ({#fold_edge_by}).
25
33
  #
26
34
  # The dispatcher returns `nil` for any case that cannot be decided from the (receiver-shape, method,
27
35
  # block-truthiness) tuple — element-wise block re-evaluation against `Constant<Array>` receivers (the
@@ -32,6 +40,16 @@ module Rigor
32
40
  FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
33
41
  FILTER_KEEP_ON_FALSEY = Set[:reject, :drop_while].freeze
34
42
 
43
+ # The filter methods `Hash` defines itself, returning a Hash. `take_while` / `drop_while` on a Hash
44
+ # are Enumerable's and return an Array of `[key, value]` pairs; `Set` and `Range` define none of the
45
+ # five, so all of theirs return an Array too.
46
+ HASH_RETURNING_FILTERS = Set[:select, :filter, :reject].freeze
47
+
48
+ # `filter_receiver_kind`'s answer for a nominal receiver; an unlisted class declines.
49
+ FILTER_NOMINAL_KINDS = {
50
+ "Array" => :array, "Hash" => :hash, "Set" => :enumerable, "Range" => :enumerable
51
+ }.freeze
52
+
35
53
  PREDICATE_METHODS = Set[:all?, :any?, :none?].freeze
36
54
 
37
55
  # Methods whose answer is `nil` when the block always returns Ruby-falsey — `find` / `detect`
@@ -46,6 +64,10 @@ module Rigor
46
64
  # a finitely-sized receiver it is `Constant[size]`.
47
65
  COUNT_METHOD = :count
48
66
 
67
+ # `min_by` / `max_by` pick one element by the block's key, so the block's value decides WHICH element
68
+ # but never WHETHER one is returned: only an empty receiver answers `nil`.
69
+ EDGE_BY_METHODS = Set[:min_by, :max_by].freeze
70
+
49
71
  # `context.block_type` is the inferred return type of the call's block; `nil` means "no block at the
50
72
  # call site" and disqualifies every rule here.
51
73
  def try_dispatch(context)
@@ -54,12 +76,13 @@ module Rigor
54
76
  args = context.args
55
77
  block_type = context.block_type
56
78
  return nil if receiver.nil? || block_type.nil?
79
+ return fold_edge_by(receiver, args) if EDGE_BY_METHODS.include?(method_name)
57
80
 
58
81
  truthiness = constant_truthiness(block_type)
59
82
  return nil if truthiness.nil?
60
83
 
61
84
  if PREDICATE_METHODS.include?(method_name)
62
- fold_predicate(receiver, method_name, truthiness)
85
+ fold_predicate(receiver, method_name, truthiness, args)
63
86
  elsif filter_method?(method_name)
64
87
  fold_filter(receiver, method_name, truthiness)
65
88
  elsif FALSEY_BLOCK_NIL_METHODS.include?(method_name)
@@ -69,6 +92,22 @@ module Rigor
69
92
  end
70
93
  end
71
94
 
95
+ # Issue #1333 — `min_by` / `max_by` with a block and no count on a non-empty Tuple or a non-empty
96
+ # constant integer `Range` answer the receiver's element type: the RBS `Elem?` return adds `nil` for
97
+ # the empty receiver, and `[1, 2].min_by { |s| rand(3) } + 1` then reported a nil receiver on correct
98
+ # code. The block's value picks which element, and one `block_type` typed from the call's entry scope
99
+ # cannot say which, so the answer is the whole element union. An empty receiver, one whose size is
100
+ # not static (`Array[T]`), and the count form (`min_by(2) { … }`, an Array) decline to RBS.
101
+ def fold_edge_by(receiver, args)
102
+ return nil unless args.empty?
103
+ return nil unless receiver_emptiness(receiver) == :non_empty
104
+
105
+ case receiver
106
+ when Type::Tuple then Type::Combinator.union(*receiver.elements)
107
+ when Type::Constant then IteratorDispatch.element_type_of(receiver) if receiver.value.is_a?(Range)
108
+ end
109
+ end
110
+
72
111
  def filter_method?(method_name)
73
112
  FILTER_KEEP_ON_TRUTHY.include?(method_name) ||
74
113
  FILTER_KEEP_ON_FALSEY.include?(method_name)
@@ -85,15 +124,28 @@ module Rigor
85
124
  block_type.value ? :truthy : :falsey
86
125
  end
87
126
 
88
- # Filter-shaped methods collapse to either the receiver (every element kept) or the empty tuple
89
- # (every element dropped). Tuple-shaped receivers widen to `Array[union of elements]` on the
90
- # all-kept side because we cannot prove WHICH positional subset survives — Tuple's per-position
91
- # semantics do not carry over to a filtered Array.
127
+ # Filter-shaped methods collapse to either the receiver (every element kept) or the empty collection
128
+ # (every element dropped), and the collection's class is the method's result class, not the
129
+ # receiver's: `{ a: 1 }.reject { true }` is `{}`, `{ a: 1 }.take_while { false }` is `[]`.
130
+ #
131
+ # The all-kept side answers the receiver only when the result class matches it — an Array for an
132
+ # Array receiver, a Hash for a Hash receiver's own filters. Otherwise (`Set[1].select { true }` is
133
+ # `[1]`, `{ a: 1 }.take_while { true }` is `[[:a, 1]]`) it declines so the RBS tier's
134
+ # `Array[Elem]` projection answers. Tuple-shaped receivers widen to `Array[union of elements]`
135
+ # on the all-kept side because we cannot prove WHICH positional subset survives — Tuple's
136
+ # per-position semantics do not carry over to a filtered Array.
92
137
  def fold_filter(receiver, method_name, truthiness)
93
- return nil unless filter_receiver_known?(receiver)
138
+ kind = filter_receiver_kind(receiver)
139
+ return nil if kind.nil?
94
140
 
95
- keep_all = filter_keeps_all?(method_name, truthiness)
96
- keep_all ? receiver_as_kept_array(receiver) : Type::Combinator.tuple_of
141
+ hash_result = kind == :hash && HASH_RETURNING_FILTERS.include?(method_name)
142
+ unless filter_keeps_all?(method_name, truthiness)
143
+ return hash_result ? Type::Combinator.hash_shape_of({}) : Type::Combinator.tuple_of
144
+ end
145
+ return receiver if hash_result
146
+ return receiver_as_kept_array(receiver) if kind == :array
147
+
148
+ nil
97
149
  end
98
150
 
99
151
  def filter_keeps_all?(method_name, truthiness)
@@ -117,8 +169,11 @@ module Rigor
117
169
  end
118
170
 
119
171
  # Predicate folds. The decision table mirrors Ruby's actual semantics on `Enumerable#all?` / `#any?` /
120
- # `#none?` — see the table at the top of the module.
121
- def fold_predicate(receiver, method_name, truthiness)
172
+ # `#none?` — see the table at the top of the module. A pattern argument declines: Ruby then tests
173
+ # `pattern === element` and ignores the block, so `[1, 2].all?(String) { true }` is `false`.
174
+ def fold_predicate(receiver, method_name, truthiness, args)
175
+ return nil unless args.empty?
176
+
122
177
  emptiness = receiver_emptiness(receiver)
123
178
  decision = predicate_decision(method_name, truthiness, emptiness)
124
179
  return nil if decision.nil?
@@ -216,14 +271,20 @@ module Rigor
216
271
  type.is_a?(Type::Nominal) && %w[Array Hash Set].include?(type.class_name)
217
272
  end
218
273
 
219
- # Filter folds need at least a recognised collection carrier; `Top` / `Dynamic` / arbitrary nominals
220
- # decline so the RBS tier answers (its `Array#select { … } -> Array[T]` projection is correct, just
221
- # less precise on the empty endpoint).
222
- def filter_receiver_known?(receiver)
274
+ # Classifies a filter receiver as `:array`, `:hash`, or `:enumerable` (a `Set` or `Range`, whose
275
+ # filters are all Enumerable's). Filter folds need at least a recognised collection carrier, so
276
+ # `Top` / `Dynamic` / arbitrary nominals / non-collection constants answer `nil` and decline so the
277
+ # RBS tier answers (its `Array#select { … } -> Array[T]` projection is correct, just less precise
278
+ # on the empty endpoint). A `Difference` (`non-empty-array[T]`, `non-empty-hash[K, V]`) classifies
279
+ # by its base. The `Constant` receivers are the ones `constant_emptiness` reads: `Range` literals
280
+ # and folded `Set` values.
281
+ def filter_receiver_kind(receiver)
223
282
  case receiver
224
- when Type::Tuple, Type::HashShape, Type::Constant, Type::Difference then true
225
- when Type::Nominal then %w[Array Hash Set Range].include?(receiver.class_name)
226
- else false
283
+ when Type::Tuple then :array
284
+ when Type::HashShape then :hash
285
+ when Type::Constant then :enumerable if receiver.value.is_a?(Range) || receiver.value.is_a?(::Set)
286
+ when Type::Difference then filter_receiver_kind(receiver.base)
287
+ when Type::Nominal then FILTER_NOMINAL_KINDS[receiver.class_name]
227
288
  end
228
289
  end
229
290