rigortype 0.3.8 → 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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "../reflection"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #1095 — the first slice of the block call-timing summary that
9
+ # `docs/type-specification/control-flow-analysis.md` § "Block call timing" asks for: which callees invoke
10
+ # their block **immediately, exactly once, and before they return**.
11
+ #
12
+ # The fact matters for the value of the call. Since #853 a block-level `break` joins the yielding call's
13
+ # type, unioned with what the callee returns when the block completes normally. For an arbitrary callee
14
+ # that normal return must stay — `each` may never yield on an empty receiver. For a callee that always
15
+ # yields exactly once before returning, a block whose normal completion is unreachable means the normal
16
+ # return is unreachable too, so the call's value is its `break` arms alone (`bot` when there are none):
17
+ # `[1, 2].tap { break "s" }` is `"s"`, `[1, 2].tap { raise "x" }` is `bot`.
18
+ #
19
+ # ## Catalogue
20
+ #
21
+ # Like {ClosureEscapeAnalyzer}'s `OBJECT_NON_ESCAPING` — a weaker "block is not retained" fact that `each`
22
+ # and `map` also satisfy — this ships as a hardcoded table keyed by `(owner, method)`, where the owner is
23
+ # the module whose declaration Ruby dispatches to. The sub-phase that replaces the escape catalogue with an
24
+ # `RBS::Extended` call-timing effect (`rbs-extended.md` reserves the bundle slot) is expected to absorb
25
+ # this table in the same move, which is why it is keyed by the declaring owner, the way a signature-borne
26
+ # effect would be, rather than by the receiver's class.
27
+ #
28
+ # The owner is `Kernel` for all three, in CRuby (`Object.instance_method(:tap).owner`) and in the bundled
29
+ # core RBS alike. `Object` is deliberately NOT accepted: every class inherits these methods through
30
+ # `Object`, so a declaration that resolves to `Object` itself is a project or gem override written on top
31
+ # of Kernel's, and that override is not known to yield exactly once.
32
+ #
33
+ # The summary is a pure query. It never raises on unrecognised input and answers `false` — keep the
34
+ # `break | normal-return` union — whenever it cannot prove the call reaches the catalogued declaration.
35
+ module BlockCallTiming
36
+ EXACTLY_ONCE_IMMEDIATE = {
37
+ "Kernel" => %i[tap then yield_self].freeze
38
+ }.freeze
39
+
40
+ CANDIDATE_NAMES = EXACTLY_ONCE_IMMEDIATE.values.flatten.uniq.freeze
41
+ private_constant :CANDIDATE_NAMES
42
+
43
+ # The roots a project monkey-patch would redefine the method on for every receiver at once.
44
+ PATCHABLE_ROOTS = %w[Object Kernel BasicObject].freeze
45
+ private_constant :PATCHABLE_ROOTS
46
+
47
+ # A class object's own metaclass ancestry adds these ahead of `Object`, so a patch on either one
48
+ # redefines the method for every class-object receiver.
49
+ SINGLETON_ROOTS = %w[Class Module].freeze
50
+ private_constant :SINGLETON_ROOTS
51
+
52
+ # Receiver-less (or `self.` / `Kernel.`) calls that never return normally: they raise, throw, or end
53
+ # the process. Anything else — including `loop`, whose RBS return is `bot` although a `StopIteration`
54
+ # ends it normally ({.loop_may_complete?}) — proves nothing.
55
+ NON_RETURNING_CALLS = %i[raise fail throw exit exit! abort].freeze
56
+ private_constant :NON_RETURNING_CALLS
57
+
58
+ # Issue #1107 — the nodes a `loop` body may consist of while still provably unable to raise
59
+ # `StopIteration`: none of them dispatches a method, so nothing in the body can run code that raises.
60
+ # Everything else — every call (operators and `[]` included), `yield`, `super`, interpolation, a splat,
61
+ # a constant read (`const_missing`), a `rescue` — may, and declines.
62
+ STOP_ITERATION_FREE_NODES = [
63
+ Prism::StatementsNode, Prism::ParenthesesNode, Prism::ArgumentsNode,
64
+ Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::ReturnNode,
65
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
66
+ Prism::StringNode, Prism::SymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode, Prism::SelfNode,
67
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode,
68
+ Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode,
69
+ Prism::IfNode, Prism::UnlessNode, Prism::ElseNode, Prism::AndNode, Prism::OrNode, Prism::ArrayNode
70
+ ].freeze
71
+ private_constant :STOP_ITERATION_FREE_NODES
72
+
73
+ module_function
74
+
75
+ # Issue #1107 — whether a `loop` call can complete normally although its declared return is `bot`.
76
+ #
77
+ # `Kernel#loop` is `() { () -> void } -> bot` in core RBS, yet it rescues a `StopIteration` its block
78
+ # raises and returns that exception's `result` — the enumerator-draining idiom `loop { out << e.next }`
79
+ # returns `e`'s own `each` value. So the declared `bot` holds only for a body that provably cannot raise
80
+ # `StopIteration`: one built from {STOP_ITERATION_FREE_NODES} alone (`loop {}`, `loop { break 1 }`), in a
81
+ # block that declares no parameters. A `&blk` block-pass has no body to prove anything about and completes
82
+ # too.
83
+ #
84
+ # Gated on the spellings that reach the private `Kernel#loop` — receiver-less, `self.`, `Kernel.`,
85
+ # `::Kernel.` — so `obj.loop { ... }` is some other method, whose declared `bot` is that author's
86
+ # promise. A project redefinition of `loop` itself is not excluded: widening its `bot` costs precision,
87
+ # never a false positive.
88
+ def loop_may_complete?(call_node)
89
+ return false unless call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
90
+
91
+ block = call_node.block
92
+ return false if block.nil?
93
+ return true unless block.is_a?(Prism::BlockNode)
94
+ # A parameter default (`|v = e.next|`) is evaluated on every iteration, since `loop` yields no
95
+ # arguments. Rather than walk the parameter list, any declared parameter widens.
96
+ return true if block.parameters
97
+ return false if block.body.nil?
98
+
99
+ may_raise_stop_iteration?(block.body)
100
+ end
101
+
102
+ def may_raise_stop_iteration?(node)
103
+ return true unless STOP_ITERATION_FREE_NODES.any? { |klass| node.is_a?(klass) }
104
+
105
+ node.compact_child_nodes.any? { |child| may_raise_stop_iteration?(child) }
106
+ end
107
+
108
+ # Cheap name-only pre-gate, so a call that cannot be catalogued pays nothing further.
109
+ def candidate_name?(method_name)
110
+ CANDIDATE_NAMES.include?(method_name)
111
+ end
112
+
113
+ def exactly_once_owner?(owner_name, method_name)
114
+ methods = EXACTLY_ONCE_IMMEDIATE[owner_name.to_s.delete_prefix("::")]
115
+ methods ? methods.include?(method_name) : false
116
+ end
117
+
118
+ # Whether a call of `method_name` on `receiver_type` reaches a catalogued exactly-once declaration.
119
+ #
120
+ # Gated on the RESOLVED owner, not the name: a class that defines its own `tap` (in source, in a
121
+ # project `sig/`, or through a project ancestor), a top-level `def tap` (a private `Object` method,
122
+ # ahead of `Kernel` in every MRO) and an `Object` / `Kernel` monkey-patch all decline. A union receiver
123
+ # qualifies only when every member does; `Dynamic` and the other carriers that name no class decline.
124
+ def exactly_once_call?(receiver_type:, method_name:, scope:)
125
+ return false unless candidate_name?(method_name)
126
+ return false if scope.nil? || project_redefines_root?(method_name, scope)
127
+
128
+ targets = receiver_targets(receiver_type)
129
+ return false if targets.nil? || targets.empty?
130
+
131
+ targets.all? { |class_name, kind| resolves_to_catalogue?(class_name, kind, method_name, scope) }
132
+ rescue StandardError
133
+ false
134
+ end
135
+
136
+ # Syntactic proof that a block body cannot complete normally: every path through it ends in a
137
+ # block-level `break` (which targets the yielding call), `return`, `redo`, or a non-returning Kernel call
138
+ # ({NON_RETURNING_CALLS}) — or in an expression that must evaluate one of those first. This is ANDed
139
+ # with the block-return pass's `bot`, never used alone, because a `bot` can also flow out of a callee's
140
+ # declared return that is not a promise the call never completes (`Kernel#loop` is `-> bot`, yet a
141
+ # `StopIteration` from `e.next` ends it normally). A shape the walk does not recognise declines.
142
+ #
143
+ # Only unconditionally evaluated children are descended: a nested block, lambda, `def` or loop is never
144
+ # entered (its body may not run, and it retargets `break`), a call's block is not a child the call
145
+ # promises to run, and `&&` / `||` count only their left operand. A `begin` with `rescue` qualifies only
146
+ # when its body and every rescue clause must exit; an `ensure` that must exit qualifies on its own.
147
+ def never_completes_normally?(node, scope)
148
+ case node
149
+ when Prism::StatementsNode then node.body.any? { |statement| never_completes_normally?(statement, scope) }
150
+ when Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode then true
151
+ when Prism::ParenthesesNode then never_completes_normally?(node.body, scope)
152
+ when Prism::CallNode then call_never_returns?(node, scope)
153
+ when Prism::IfNode, Prism::UnlessNode then conditional_never_completes?(node, scope)
154
+ when Prism::AndNode, Prism::OrNode then never_completes_normally?(node.left, scope)
155
+ when Prism::BeginNode then begin_never_completes?(node, scope)
156
+ when Prism::ArrayNode then node.elements.any? { |element| never_completes_normally?(element, scope) }
157
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
158
+ never_completes_normally?(node.value, scope)
159
+ else false
160
+ end
161
+ end
162
+
163
+ # Deliberately coarse: ANY project definition of the name — top-level, on any class or module, either
164
+ # side, or a `pre_eval:` patch — declines. Resolving the call-site `self`'s ancestry precisely buys
165
+ # nothing for names this rare, and the block-return pass is no independent check here: it types a
166
+ # self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
167
+ # {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
168
+ def project_defines_anywhere?(method_name, scope)
169
+ return true if scope.top_level_def_for(method_name)
170
+ # `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
171
+ # def-node tables still carry it, on both sides.
172
+ return true if [scope.discovered_methods, scope.discovered_def_nodes, scope.discovered_singleton_def_nodes]
173
+ .any? { |tables| tables.any? { |_class_name, table| table.key?(method_name) } }
174
+
175
+ patched = scope.environment&.project_patched_methods
176
+ !patched.nil? && patched.by_key.any? { |(_class_name, name, _kind), _entry| name == method_name }
177
+ end
178
+
179
+ class << self
180
+ private
181
+
182
+ def call_never_returns?(node, scope)
183
+ return true if never_completes_normally?(node.receiver, scope)
184
+ return true if node.arguments&.arguments&.any? { |argument| never_completes_normally?(argument, scope) }
185
+ return false unless NON_RETURNING_CALLS.include?(node.name)
186
+
187
+ kernel_spelled_receiver?(node.receiver) && !project_defines_anywhere?(node.name, scope)
188
+ end
189
+
190
+ # Implicit self, `self.`, or the `Kernel` module itself (`Kernel.` or the root-anchored `::Kernel.`) —
191
+ # the spellings that reach Kernel's function.
192
+ def kernel_spelled_receiver?(receiver)
193
+ case receiver
194
+ when nil, Prism::SelfNode then true
195
+ when Prism::ConstantReadNode then receiver.name == :Kernel
196
+ when Prism::ConstantPathNode then receiver.parent.nil? && receiver.name == :Kernel
197
+ else false
198
+ end
199
+ end
200
+
201
+ def conditional_never_completes?(node, scope)
202
+ return true if never_completes_normally?(node.predicate, scope)
203
+
204
+ alternative = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
205
+ return false if node.statements.nil? || alternative.nil?
206
+
207
+ never_completes_normally?(node.statements, scope) && branch_never_completes?(alternative, scope)
208
+ end
209
+
210
+ def branch_never_completes?(branch, scope)
211
+ case branch
212
+ when Prism::ElseNode then never_completes_normally?(branch.statements, scope)
213
+ else never_completes_normally?(branch, scope)
214
+ end
215
+ end
216
+
217
+ def begin_never_completes?(node, scope)
218
+ return true if node.ensure_clause && never_completes_normally?(node.ensure_clause.statements, scope)
219
+ return false unless never_completes_normally?(node.statements, scope)
220
+
221
+ rescue_clause = node.rescue_clause
222
+ while rescue_clause
223
+ return false unless never_completes_normally?(rescue_clause.statements, scope)
224
+
225
+ rescue_clause = rescue_clause.subsequent
226
+ end
227
+ true
228
+ end
229
+
230
+ def project_redefines_root?(method_name, scope)
231
+ return true if scope.top_level_def_for(method_name)
232
+
233
+ patched = scope.environment&.project_patched_methods
234
+ PATCHABLE_ROOTS.any? do |root|
235
+ scope.discovered_method?(root, method_name, :instance) ||
236
+ patched&.lookup(class_name: root, method_name: method_name, kind: :instance)
237
+ end
238
+ end
239
+
240
+ # `[class_name, :instance | :singleton]` pairs the receiver can dispatch through, or nil when some
241
+ # part of it names no class. `Tuple` / `HashShape` project to `Array` / `Hash`, `Constant[v]` through
242
+ # the value's class, the same projection {ClosureEscapeAnalyzer} uses.
243
+ def receiver_targets(receiver_type)
244
+ case receiver_type
245
+ when Type::Union
246
+ members = receiver_type.members.map { |member| receiver_targets(member) }
247
+ members.any?(&:nil?) ? nil : members.flatten(1)
248
+ when Type::Nominal then [[receiver_type.class_name, :instance]]
249
+ when Type::Singleton then [[receiver_type.class_name, :singleton]]
250
+ when Type::Tuple then [["Array", :instance]]
251
+ when Type::HashShape then [["Hash", :instance]]
252
+ when Type::Constant then [[receiver_type.value.class.name, :instance]]
253
+ end
254
+ end
255
+
256
+ def resolves_to_catalogue?(class_name, kind, method_name, scope)
257
+ return false if class_name.nil?
258
+ return false if scope.discovered_method_through_ancestors?(class_name, method_name, kind)
259
+
260
+ if kind == :singleton
261
+ definition = Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
262
+ return false if singleton_ancestor_patched?(class_name, method_name, scope)
263
+
264
+ return declared_on_catalogue?(definition, method_name)
265
+ end
266
+
267
+ definition = Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
268
+ if definition
269
+ return false if rbs_ancestor_patched?(class_name, method_name, scope)
270
+
271
+ return declared_on_catalogue?(definition, method_name)
272
+ end
273
+ return false if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
274
+
275
+ project_class_resolves_to_catalogue?(class_name, method_name, scope)
276
+ end
277
+
278
+ def declared_on_catalogue?(definition, method_name)
279
+ return false if definition.nil? || !definition.respond_to?(:defined_in)
280
+
281
+ exactly_once_owner?(definition.defined_in, method_name)
282
+ end
283
+
284
+ # A project class the RBS environment does not know: its project ancestry was already cleared by the
285
+ # discovery walk, so the answer rests on the ancestors it reaches OUTSIDE the project. Each is asked
286
+ # on its own terms; an ancestor nobody can resolve is uncertainty and declines. No external ancestor
287
+ # at all is the implicit `< Object`, which reaches `Kernel`.
288
+ def project_class_resolves_to_catalogue?(class_name, method_name, scope)
289
+ return false unless scope.known_user_class?(class_name)
290
+
291
+ scope.external_ancestor_name_candidates(class_name).all? do |candidates|
292
+ external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
293
+ end
294
+ end
295
+
296
+ def external_ancestor_resolves_to_catalogue?(candidates, method_name, scope)
297
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
298
+ return false if known.nil?
299
+
300
+ definition = Rigor::Reflection.instance_method_definition(known, method_name, scope: scope)
301
+ # A mixin whose RBS does not mention the method contributes nothing; a CLASS that lacks it is a
302
+ # `BasicObject` lineage, where the call does not reach `Kernel` at all.
303
+ return false if rbs_ancestor_patched?(known, method_name, scope)
304
+ return scope.environment.rbs_module?(known) if definition.nil?
305
+
306
+ declared_on_catalogue?(definition, method_name)
307
+ end
308
+
309
+ # The RBS declaration says where the method was DECLARED; a project reopening of a core ancestor
310
+ # (`module Enumerable; def tap = :x; end`) redefines it without touching RBS, and discovery records
311
+ # it under the ancestor's own name. So every RBS ancestor — mixins included — is asked.
312
+ def rbs_ancestor_patched?(class_name, method_name, scope)
313
+ names_patched?(rbs_ancestor_names(class_name, scope), method_name, :instance, scope)
314
+ end
315
+
316
+ # A class object dispatches through its own and its superclasses' singleton methods, then through
317
+ # `Class`, `Module`, `Object`, `Kernel` as instance methods.
318
+ def singleton_ancestor_patched?(class_name, method_name, scope)
319
+ names_patched?(rbs_ancestor_names(class_name, scope), method_name, :singleton, scope) ||
320
+ names_patched?(SINGLETON_ROOTS, method_name, :instance, scope)
321
+ end
322
+
323
+ def names_patched?(names, method_name, kind, scope)
324
+ patched = scope.environment&.project_patched_methods
325
+ names.any? do |name|
326
+ scope.discovered_method?(name, method_name, kind) ||
327
+ patched&.lookup(class_name: name, method_name: method_name, kind: kind)
328
+ end
329
+ end
330
+
331
+ def rbs_ancestor_names(class_name, scope)
332
+ loader = scope.environment&.rbs_loader
333
+ loader ? loader.ancestor_names_for(class_name.to_s) : []
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "block_auto_splat"
6
7
  require_relative "multi_target_binder"
7
8
 
8
9
  module Rigor
@@ -22,31 +23,56 @@ module Rigor
22
23
  # MultiTargetNode parameters (`|(a, b), c|`) are bound by delegating each destructuring slot to
23
24
  # {Rigor::Inference::MultiTargetBinder}, so a Tuple-shaped expected element type projects element-wise
24
25
  # into the inner locals (Slice 6 phase C sub-phase 2). Numbered parameters (`_1`, `_2`, ...) are bound
25
- # from `Prism::NumberedParametersNode` using the same per-position `expected_param_types:` array, so
26
- # `[1, 2, 3].each { _1 + _2 }` sees `_1`/`_2` typed identically to their explicit `|x, y|` counterparts.
26
+ # from `Prism::NumberedParametersNode` as the explicit list of that many required positionals, the
27
+ # highest `_N` the body references being the arity (CRuby's own reading: `proc { _2 }.parameters` is
28
+ # `[[:opt, :_1], [:opt, :_2]]`, and `lambda { _2 }.arity` is 2). So `h.each { _1; _2 }` auto-splats like
29
+ # `|k, v|`, while a body that uses only `_1` reads as `|a|` and does not (issue #1108).
27
30
  #
28
31
  # The `it` implicit parameter (Ruby 3.4+) is bound from `Prism::ItParametersNode`. It is the
29
32
  # single-argument cousin of `_1`: the binder produces `{ it: expected_param_types[0] }` so the body's
30
- # `Prism::ItLocalVariableReadNode` lookup sees the same type as the explicit `|x|` form would.
33
+ # `Prism::ItLocalVariableReadNode` lookup sees the same type as the explicit `|x|` form would, and it
34
+ # never auto-splats.
35
+ #
36
+ # A block whose parameter list CRuby splats spreads the one yielded value across its positionals;
37
+ # {BlockAutoSplat} owns that rule and its carrier arms, and this binder only applies the table it
38
+ # answers. Every name the spread binds from an `Array[T]` slot — and every name a nested `|(g, h)|`
39
+ # destructure binds from one — is reported as optimistic, and {#bind_onto} records those through
40
+ # `Scope#with_optimistic_local` exactly as the statement-level `a, b = ints` does (issue #1093; the
41
+ # shared rule lives in {MultiTargetBinder}). A nested `|(g, h)|` destructures the one value the
42
+ # signature already placed, so it wraps like the statement form.
31
43
  #
32
44
  # Block-local declarations after `;` (e.g., `|x; y, z|`) are still skipped — they are explicitly
33
45
  # block-local, so the outer scope MUST NOT observe them and the binder leaves them unbound.
34
46
  #
35
47
  # See docs/internal-spec/inference-engine.md for the binding contract.
36
48
  class BlockParameterBinder
37
- # @param expected_param_types [Array<Rigor::Type>] positional block parameter types in order. Indices
49
+ # @param expected_param_types — positional block parameter types in order. Indices
38
50
  # the binder cannot fill from this array (because the array is shorter than the parameter list, or
39
51
  # because the slot is a kind we do not pull from the array) default to `Dynamic[Top]`.
40
52
  def initialize(expected_param_types: [])
41
- @expected_param_types = expected_param_types
53
+ @declared_param_types = expected_param_types
54
+ reset_per_bind_state
55
+ end
56
+
57
+ # The names the last {#bind} bound optimistically nil-free (see the class comment).
58
+ attr_reader :optimistic
59
+
60
+ # Binds the block's parameters into `scope`: {#bind}'s types through `Scope#with_local`, then the
61
+ # optimistic mark for every name in {#optimistic}.
62
+ def bind_onto(block_node, scope)
63
+ types = bind(block_node, scope: scope)
64
+ MultiTargetBinder::Result.new(types: types, optimistic: @optimistic.dup.freeze).apply_to(scope)
42
65
  end
43
66
 
44
- # @param block_node [Prism::BlockNode]
45
- # @return [Hash{Symbol => Rigor::Type}] ordered map from parameter name to bound type. Anonymous
67
+ # @return ordered map from parameter name to bound type. Anonymous
46
68
  # parameters are skipped; MultiTargetNode destructuring slots delegate to {MultiTargetBinder} and
47
69
  # contribute every named local in declaration order. Numbered-parameter forms (`_1`, `_2`, ...) bind
48
70
  # `:_1`, `:_2`, ... up to the maximum the block body refers to.
49
- def bind(block_node)
71
+ # @param scope — the scope the block is entered from, which answers a nested destructure's `to_ary`
72
+ # question ({MultiTargetBinder.bind_marked}).
73
+ def bind(block_node, scope: nil)
74
+ reset_per_bind_state
75
+ @scope = scope
50
76
  params_root = block_node.parameters
51
77
  return {} if params_root.nil?
52
78
 
@@ -64,14 +90,29 @@ module Rigor
64
90
 
65
91
  private
66
92
 
93
+ # {#apply_auto_splat} rewrites the positional table for the block it is binding, so every {#bind} starts
94
+ # from the declared types again; a binder reused across blocks must not see the previous block's splat.
95
+ def reset_per_bind_state
96
+ @expected_param_types = @declared_param_types
97
+ @splat_rest_type = nil
98
+ @optimistic_positions = []
99
+ @optimistic = []
100
+ @scope = nil
101
+ end
102
+
67
103
  # `|_1, _2|` numbered-parameter form. Prism exposes the implicit count through
68
104
  # `NumberedParametersNode#maximum` (the highest `_N` referenced in the body); we materialise bindings
69
- # for `:_1` through `:_maximum` so the block body's `LocalVariableReadNode` lookups see the same types
70
- # as the equivalent explicit `|x, y|` form would.
105
+ # for `:_1` through `:_maximum`, auto-splatting exactly as the explicit list of that many required
106
+ # positionals would, so the body's `LocalVariableReadNode` lookups see the same types and marks.
71
107
  def bind_numbered_parameters(numbered_node)
108
+ arity = numbered_node.maximum
109
+ apply_auto_splat(BlockAutoSplat::ParameterShape.of_arity(arity))
110
+
72
111
  bindings = {}
73
- numbered_node.maximum.times do |i|
74
- bindings[:"_#{i + 1}"] = positional_type_at(i)
112
+ arity.times do |i|
113
+ name = :"_#{i + 1}"
114
+ bindings[name] = positional_type_at(i)
115
+ @optimistic << name if @optimistic_positions.include?(i)
75
116
  end
76
117
  bindings
77
118
  end
@@ -86,7 +127,7 @@ module Rigor
86
127
  params_node = params_root.parameters
87
128
  return {} if params_node.nil?
88
129
 
89
- apply_auto_splat(params_node)
130
+ apply_auto_splat(BlockAutoSplat::ParameterShape.of(params_node))
90
131
 
91
132
  bindings = {}
92
133
  bind_positionals(params_node, bindings, 0)
@@ -97,8 +138,8 @@ module Rigor
97
138
  bindings
98
139
  end
99
140
 
100
- # Ruby blocks (NOT lambdas) auto-splat a single yielded Tuple-shaped value when the block declares more
101
- # than one required positional parameter:
141
+ # Ruby blocks (NOT lambdas) auto-splat a single yielded array-ish value when the parameter list is
142
+ # one CRuby splats:
102
143
  #
103
144
  # { a: 1 }.each { |k, v| ... }
104
145
  #
@@ -107,21 +148,21 @@ module Rigor
107
148
  # the binder would assign `k = Tuple[K, V]` and `v = Dynamic[Top]`, and any call on
108
149
  # `k.<method-not-on-Tuple>` would false-fire.
109
150
  #
110
- # The rule fires only when (a) the receiver yields exactly one value (`expected_param_types.size ==
111
- # 1`), (b) the block declares more than one positional slot, and (c) that single expected element is a
112
- # Tuple. Multi-arg yields (e.g. `each_with_index`'s `(element, index)` pair) are NOT auto-splatted —
113
- # matching Ruby semantics where a multi-arg yield to a `|a, b, c|` block fills the extra slot with nil
114
- # rather than splatting any element.
115
- def apply_auto_splat(params_node)
151
+ # The rule fires only when the receiver yields exactly one value (`expected_param_types.size == 1`)
152
+ # and {BlockAutoSplat} answers a table for that value and `shape`. A multi-arg yield (e.g.
153
+ # `each_with_index`'s `(element, index)` pair) is NOT auto-splatted — matching Ruby semantics where a
154
+ # multi-arg yield to a `|a, b, c|` block fills the extra slot with nil rather than splatting any
155
+ # element.
156
+ def apply_auto_splat(shape)
116
157
  return unless @expected_param_types.size == 1
158
+ return unless shape.splats?
117
159
 
118
- pos_count = params_node.requireds.size + params_node.optionals.size + params_node.posts.size
119
- return unless pos_count > 1
120
-
121
- first = @expected_param_types[0]
122
- return unless first.is_a?(Type::Tuple)
160
+ table = BlockAutoSplat.for(shape, @expected_param_types[0])
161
+ return if table.nil?
123
162
 
124
- @expected_param_types = first.elements
163
+ @expected_param_types = table.types
164
+ @optimistic_positions = table.optimistic_positions
165
+ @splat_rest_type = table.rest_type
125
166
  end
126
167
 
127
168
  def bind_positionals(params_node, bindings, cursor)
@@ -161,7 +202,8 @@ module Rigor
161
202
  rest = params_node.rest
162
203
  return unless rest.respond_to?(:name) && rest&.name
163
204
 
164
- bindings[rest.name] = Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
205
+ bindings[rest.name] =
206
+ @splat_rest_type || Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
165
207
  end
166
208
 
167
209
  def bind_keywords(params_node, bindings)
@@ -200,9 +242,12 @@ module Rigor
200
242
  case param
201
243
  when Prism::RequiredParameterNode
202
244
  bindings[param.name] = positional_type_at(cursor)
245
+ @optimistic << param.name if @optimistic_positions.include?(cursor)
203
246
  when Prism::MultiTargetNode
204
- nested = MultiTargetBinder.bind(param, positional_type_at(cursor))
205
- bindings.merge!(nested)
247
+ nested = MultiTargetBinder.bind_marked(param, positional_type_at(cursor),
248
+ optimistic: @optimistic_positions.include?(cursor), scope: @scope)
249
+ bindings.merge!(nested.types)
250
+ @optimistic.concat(nested.optimistic)
206
251
  end
207
252
  end
208
253
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "block_call_timing"
5
+ require_relative "closure_escape_analyzer"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Whether a block-accepting call may run its block more than once, so a later run reads a captured binding an
10
+ # earlier run moved. Two passes ask it and share this one rule: the block-return pass
11
+ # (`ExpressionTyper#block_may_repeat?`), which lays the #587 (b) captured binding under such a block, and the
12
+ # statement pass (`StatementEvaluator#repeating_block_entry`, issue #1412), which lays the ADR-56 WD2.13
13
+ # content-mutation widening on its entry.
14
+ #
15
+ # The core iteration methods {ClosureEscapeAnalyzer} catalogues as non-escaping prove it — for a project class
16
+ # too, through its ancestry (`include Enumerable`); `tap` / `then` / `yield_self` are catalogued there too but
17
+ # run their block exactly once ({BlockCallTiming}).
18
+ #
19
+ # Issue #1234 — a receiver the analyzer cannot classify at all (`Dynamic`, a union, a project class whose
20
+ # ancestry it cannot follow) repeats when the method NAME is a catalogued iterator the project does not define
21
+ # on it ({ClosureEscapeAnalyzer.repeats_by_name?}): `items.all? { seen += 1; seen == 1 }` on an untyped `items`
22
+ # kept the first run's `seen == 1`, the predicate folded to `true`, and the condition on the result was
23
+ # reported as constant. This is a reading of `:unknown` for these two passes alone; escape analysis still
24
+ # treats it as unproven. Any other name, and a receiver classified `:escaping`, answers false, which keeps the
25
+ # entry scope: it is exact for a block run once (`m.synchronize { out = buf; buf = nil; out }` is `buf`'s value,
26
+ # and a cross-iteration binding would add the `nil` a second run never reads).
27
+ #
28
+ # A receiver that provably holds at most one element runs the block at most once however it iterates, so it
29
+ # answers false too: `done = false; [:only].each { break if done; done = true }` is `[:only]`, and the
30
+ # cross-iteration `done` would have typed it `Array[Symbol]?`.
31
+ module BlockRepetition
32
+ module_function
33
+
34
+ # `classification` is the receiver's {ClosureEscapeAnalyzer.classify} answer when the caller already holds it.
35
+ def may_repeat?(method_name:, receiver_type:, scope:, classification: nil)
36
+ return false if BlockCallTiming.candidate_name?(method_name)
37
+ return false if at_most_one_run?(method_name, receiver_type)
38
+
39
+ classification ||= ClosureEscapeAnalyzer.classify(
40
+ receiver_type: receiver_type, method_name: method_name, scope: scope
41
+ )
42
+ case classification
43
+ when :non_escaping then true
44
+ when :unknown
45
+ ClosureEscapeAnalyzer.repeats_by_name?(receiver_type: receiver_type, method_name: method_name, scope: scope)
46
+ else false
47
+ end
48
+ end
49
+
50
+ def at_most_one_run?(method_name, receiver_type)
51
+ case receiver_type
52
+ when Type::Tuple then receiver_type.elements.size <= 1
53
+ when Type::HashShape then receiver_type.closed? && receiver_type.pairs.size <= 1
54
+ when Type::Constant then constant_at_most_one_run?(method_name, receiver_type.value)
55
+ else false
56
+ end
57
+ end
58
+
59
+ # `1.times` and a one-element integer range iterate once.
60
+ def constant_at_most_one_run?(method_name, value)
61
+ case value
62
+ when Integer then method_name == :times && value <= 1
63
+ when Range
64
+ value.begin.is_a?(Integer) && value.end.is_a?(Integer) &&
65
+ value.end - value.begin + (value.exclude_end? ? 0 : 1) <= 1
66
+ else false
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -19,19 +19,20 @@ module Rigor
19
19
  # The mechanism is parameterized over an `evaluate_body` callable so slice B can reuse it: given the
20
20
  # current per-name bindings it returns the per-name exit bindings produced by one body evaluation from
21
21
  # those bindings (names the body leaves unwritten in a given pass simply do not appear in the returned
22
- # hash).
22
+ # hash). Slice C's content join reuses it too, with each "name" one evidence slot of a collection
23
+ # (`StatementEvaluator#join_content_to_fixpoint`) and a `bot` seed standing for "nothing stored yet".
23
24
  module BodyFixpoint
24
25
  # One body evaluation per iteration; ADR-55's shape (cap 3).
25
26
  CAP = 3
26
27
 
27
28
  module_function
28
29
 
29
- # @param names [Array<Symbol>] the outer locals the body can rebind.
30
- # @param seed_bindings [Hash{Symbol=>Type}] pre-state binding per name.
31
- # @param widen [#call] value-pinned widener (Constant -> Nominal).
32
- # @param evaluate_body [#call] `bindings -> exit_bindings` — evaluates the body once from `bindings`
30
+ # @param names — the outer locals the body can rebind.
31
+ # @param seed_bindings — pre-state binding per name.
32
+ # @param widen — value-pinned widener (Constant -> Nominal).
33
+ # @param evaluate_body — `bindings -> exit_bindings` — evaluates the body once from `bindings`
33
34
  # (the per-name current assumption) and returns the per-name exit binding it produced.
34
- # @return [Hash{Symbol=>Type}] the continuation binding per name.
35
+ # @return the continuation binding per name.
35
36
  def converge(names:, seed_bindings:, widen:, evaluate_body:)
36
37
  return {} if names.empty?
37
38
 
@@ -49,7 +49,8 @@ module Rigor
49
49
  RECURSION_FIXPOINT_CAP = :recursion_fixpoint_cap
50
50
  # `BodyFixpoint#converge` (ADR-56 slice A — non-escaping block captured-local write-back) ran its
51
51
  # 3-evaluation cap without the written local's join converging and collapsed that local to
52
- # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint.
52
+ # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint and by slice C's
53
+ # content-evidence fixpoint, where the collapsed name is one element / key / value slot of a collection join.
53
54
  BLOCK_WRITEBACK_CAP = :block_writeback_cap
54
55
 
55
56
  # ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
@@ -65,8 +66,6 @@ module Rigor
65
66
  # plain `(receiver, method)` signature was already on the recursion guard stack.
66
67
  # - {MEMO_REFUSE_UNROLL} — pre-ADR-84 blanket refusal (constant-arg unroll in flight). Kept in the table
67
68
  # as the ADR-84 WD3 gate pin: structurally unreachable, must read ~0.
68
- # - {MEMO_REFUSE_CONSULT_TAINTED} — a candidate frame computed a result but an ADR-55 fixpoint summary
69
- # was *consulted* during the compute, so the result is a transient Kleene iterate and was NOT stored.
70
69
  # - {MEMO_REFUSE_TRANSIENT} — ADR-84 WD3: a candidate frame's compute bracket saw a
71
70
  # transient-machinery event (recursion-guard hit / unroll-fuel exhaustion / ADR-55 WD1 clamp /
72
71
  # fixpoint-cap collapse — `ExpressionTyper#note_transient_fallback`) that referenced a stack frame
@@ -79,15 +78,13 @@ module Rigor
79
78
  MEMO_BODY_EVALS = :memo_body_evals
80
79
  MEMO_REFUSE_ON_STACK = :memo_refuse_on_stack
81
80
  MEMO_REFUSE_UNROLL = :memo_refuse_unroll
82
- MEMO_REFUSE_CONSULT_TAINTED = :memo_refuse_consult_tainted
83
81
  MEMO_REFUSE_TRANSIENT = :memo_refuse_transient
84
82
 
85
83
  CATEGORIES = [
86
84
  RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
87
85
  RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
88
86
  MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
89
- MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_CONSULT_TAINTED,
90
- MEMO_REFUSE_TRANSIENT
87
+ MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_TRANSIENT
91
88
  ].freeze
92
89
 
93
90
  # Distribution (histogram) categories — read-only observations of a value's size at a site, used to