rigortype 0.4.5 → 0.4.7

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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../run_scoped_slots"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The value the statement evaluator last gave a loop node, and the scope it evaluated the loop from, so
8
+ # `ExpressionTyper#type_of_loop` reads it instead of evaluating the loop again (issue #2031 review). A loop's value
9
+ # costs an evaluation of its body, its fixpoint and its `break` passes included, and the typer types a loop again
10
+ # wherever it types a node that holds one: the value of a write that wraps it (`id(z = (while …))`), a call it is
11
+ # the receiver of (`(while …).itself`), each narrowing and rule that types that operand. Each such typing of a
12
+ # loop nested in another loop re-ran the inner loop's passes, once more per level of nesting.
13
+ #
14
+ # The record is reused only for the very scope object the loop was evaluated from, so it answers exactly what an
15
+ # evaluation would. It is not compared with `Scope#==`, which ignores several fields (`match_frame`,
16
+ # `declared_refinements`, `struct_fold_safe_locals`, the opaque-block flags, `lexical_nesting`, the origins tables,
17
+ # `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
18
+ # all (issue #2262). It lives in a per-thread map keyed by the loop node. The map's keys are weak but its values
19
+ # are strong, and a recorded scope reaches its discovery, its environment and the parse tree that holds the key,
20
+ # so a record never goes on its own (issue #2412): the map is a run-scoped slot ({RunScopedSlots}), emptied when
21
+ # the run that filled it ends.
22
+ module LoopValueRecord
23
+ KEY = RunScopedSlots.register(:rigor_loop_value_record)
24
+ private_constant :KEY
25
+
26
+ module_function
27
+
28
+ COUNT_KEY = RunScopedSlots.register(:rigor_loop_value_count)
29
+ private_constant :COUNT_KEY
30
+
31
+ def record(node, scope, type)
32
+ Thread.current[COUNT_KEY] = loops_evaluated + 1
33
+ conditional(node, scope, type)
34
+ end
35
+
36
+ # How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
37
+ def loops_evaluated = Thread.current[COUNT_KEY] || 0
38
+
39
+ # The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
40
+ # conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
41
+ # branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
42
+ def conditional(node, scope, type)
43
+ (Thread.current[KEY] ||= ObjectSpace::WeakKeyMap.new)[node] = [scope, type]
44
+ type
45
+ end
46
+
47
+ # The recorded value of `node` evaluated from `scope` itself, or nil.
48
+ def fetch(node, scope)
49
+ slot = Thread.current[KEY]&.[](node)
50
+ return nil if slot.nil?
51
+
52
+ recorded, type = slot
53
+ recorded.equal?(scope) ? type : nil
54
+ end
55
+ end
56
+ end
57
+ end
@@ -15,6 +15,12 @@ module Rigor
15
15
  # `$_` (issue #1359), whose frame-wide answers ({#last_line_closure?}, {#last_line_fallback?}) are kept here
16
16
  # too, asked only while `$_` is narrowed.
17
17
  class Frame
18
+ # The body the frame runs, which issue #2555's escape scan reads ({ConstructionEscape.escaped?}).
19
+ attr_reader :body
20
+ # The method's parameters (nil for a class, module or file body), which issue #2608's holder scan reads for a
21
+ # zero-argument `super` ({RemoverHolderScan}).
22
+ attr_reader :parameters
23
+
18
24
  # The nodes that bind a local in a body: the writes, and a block's, lambda's, `rescue`'s or pattern's own
19
25
  # parameters and targets.
20
26
  LOCAL_BINDINGS = Set[
@@ -37,6 +43,8 @@ module Rigor
37
43
  @scans = nil
38
44
  @last_line_closure = nil
39
45
  @last_line_fallback = nil
46
+ @body_locals = nil
47
+ @parameter_names = nil
40
48
  end
41
49
 
42
50
  def matching_closure?(scope = nil)
@@ -88,6 +96,29 @@ module Rigor
88
96
  @forwarded_block == name
89
97
  end
90
98
 
99
+ # Issue #2354 — whether `name` is a local of the frame's own body: the body binds it (a write, or a block's,
100
+ # lambda's, `rescue`'s or pattern's own parameter or target) and no parameter of the method names it. A
101
+ # method parameter is the caller's object wherever the body also writes the name (`h = h.dup if f`, a
102
+ # later `h = {}`), so it never counts. Kept per name for the rest of the body.
103
+ def body_local?(name)
104
+ bound = (@body_locals ||= {})
105
+ return bound[name] if bound.key?(name)
106
+
107
+ bound[name] = !parameter_names.include?(name) && binds_local?(@body, name)
108
+ end
109
+
110
+ # Issue #1157 — whether a call's `&expr` block argument hands on the block THIS method was called with: the
111
+ # anonymous `&` of a method that declares an anonymous `&` parameter, or {#forwarded_block?} for `&name`.
112
+ # Anything else (`&:sym`, a proc made here, a rebound name) is a block of this frame's own making.
113
+ def forwards_block?(block_argument)
114
+ expression = block_argument.expression
115
+ case expression
116
+ when nil then anonymous_block_parameter?
117
+ when Prism::LocalVariableReadNode then forwarded_block?(expression.name)
118
+ else false
119
+ end
120
+ end
121
+
91
122
  # The scan of `node` under `scope`, kept while `scope`'s local and instance-variable tables are the same
92
123
  # objects: a lookup argument's answer reads them ({MatchRebinding.may_match?}), and a rebuild that leaves
93
124
  # them alone passes the same tables on. `kind` keeps another reading of the same node apart (issue #1365:
@@ -125,6 +156,40 @@ module Rigor
125
156
  name
126
157
  end
127
158
 
159
+ # True when the method declares a bare `&` parameter, which only an anonymous `&` argument can read.
160
+ def anonymous_block_parameter?
161
+ parameters = @parameters
162
+ block = parameters.is_a?(Prism::ParametersNode) ? parameters.block : nil
163
+ !block.nil? && block.name.nil?
164
+ end
165
+
166
+ # The names the method's own parameters bind: positional (destructured ones included), optional, rest,
167
+ # keyword, keyword-rest and block. Empty for a class, module or file body.
168
+ def parameter_names
169
+ @parameter_names ||= begin
170
+ parameters = @parameters
171
+ names = Set.new
172
+ if parameters.is_a?(Prism::ParametersNode)
173
+ [*parameters.requireds, *parameters.optionals, parameters.rest, *parameters.posts,
174
+ *parameters.keywords, parameters.keyword_rest, parameters.block].each do |parameter|
175
+ collect_parameter_names(parameter, names)
176
+ end
177
+ end
178
+ names.freeze
179
+ end
180
+ end
181
+
182
+ def collect_parameter_names(node, names)
183
+ case node
184
+ when Prism::MultiTargetNode
185
+ [*node.lefts, node.rest, *node.rights].each { |target| collect_parameter_names(target, names) }
186
+ when Prism::SplatNode then collect_parameter_names(node.expression, names)
187
+ else
188
+ name = node.respond_to?(:name) ? node.name : nil
189
+ names << name if name
190
+ end
191
+ end
192
+
128
193
  # The method's own `&block` parameter's name, or nil.
129
194
  def block_parameter_name
130
195
  parameters = @parameters
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MatchRebinding
8
+ # Issue #2443 — the match globals a block enters with when the call that owns it matches before every yield.
9
+ # `String#scan`, `#gsub`, `#gsub!`, `#sub` and `#sub!` set the caller's `$~` to the current match before each
10
+ # run of their block, with a Regexp pattern or a String one, so the block reads that match on every run
11
+ # whatever the enclosing frame held; the frame-wide entry ({MatchRebinding.block_entry}) would otherwise leave
12
+ # it the narrowing at the call site (issue #1371) or nothing at all, and `Regexp.last_match.begin(0)` in the
13
+ # block read `MatchData?`.
14
+ #
15
+ # A receiver that is a String, or a `Dynamic` the analyzer cannot type ({Narrowing.string_match_receiver?}),
16
+ # enters with the call's own match edge ({Narrowing.yielded_match_scope}). A `Dynamic` receiver may be another
17
+ # object whose block-taking `scan` runs no match, and then the block reads the caller's `$~`; narrowing it
18
+ # anyway can only drop a diagnostic, never add one (ADR-5). A receiver known to be anything else — a project
19
+ # class, `Pathname`, a union with either — keeps the frame-wide entry: a method defined in Ruby runs its
20
+ # matches in a frame of its own, so its block reads the caller's `$~` as the frame rules leave it. So does an
21
+ # implicit-self call.
22
+ module YieldingMatch
23
+ NAMES = Set[:scan, :gsub, :gsub!, :sub, :sub!].freeze
24
+ private_constant :NAMES
25
+
26
+ module_function
27
+
28
+ # `scope` with the match globals `call_node`'s block enters with; `scope` itself for any other block.
29
+ # `operand_scope` types the receiver.
30
+ def entry(scope, block_node, call_node, operand_scope)
31
+ return scope unless yielding_call?(block_node, call_node) &&
32
+ Narrowing.string_match_receiver?(operand_scope.type_of(call_node.receiver), operand_scope)
33
+
34
+ Narrowing.yielded_match_scope(scope)
35
+ end
36
+
37
+ def yielding_call?(block_node, call_node)
38
+ call_node.is_a?(Prism::CallNode) && NAMES.include?(call_node.name) && !call_node.receiver.nil? &&
39
+ block_node.is_a?(Prism::BlockNode) && call_node.block.equal?(block_node)
40
+ end
41
+ private_class_method :yielding_call?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -12,6 +12,7 @@ require_relative "match_rebinding/frame"
12
12
  require_relative "match_rebinding/operands"
13
13
  require_relative "match_rebinding/calls"
14
14
  require_relative "match_rebinding/self_calls"
15
+ require_relative "match_rebinding/yielding_match"
15
16
  require_relative "last_line"
16
17
 
17
18
  module Rigor
@@ -367,12 +368,21 @@ module Rigor
367
368
  #
368
369
  # Issue #1429 — the entry also restores a guard's narrowing of a global or constant where the body may run after
369
370
  # code that rebinds it ({GuardRebinding.block_entry}).
371
+ #
372
+ # Issue #2443 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, …) on a String or
373
+ # untyped receiver enters with that call's own match edge, laid over the entry above ({YieldingMatch}); a match
374
+ # its body runs still rebinds them through the statement-level rules.
370
375
  def block_entry(scope, block_node, call_node = nil)
376
+ operand_scope = scope
371
377
  scope = GuardRebinding.block_entry(scope, block_node, call_node)
372
378
  return FreshFrameBlocks.entry(scope, call_node) if FreshFrameBlocks.fresh_entry?(call_node, scope)
373
379
 
374
380
  scope = FreshFrameBlocks.closure_entry(scope, block_node, call_node)
375
381
  scope = LastLine.block_entry(scope, block_node, call_node)
382
+ YieldingMatch.entry(frame_entry(scope, block_node, call_node), block_node, call_node, operand_scope)
383
+ end
384
+
385
+ def frame_entry(scope, block_node, call_node)
376
386
  return scope unless scope.match_globals_bound?
377
387
  return scope unless scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
378
388
  (call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
@@ -386,7 +396,7 @@ module Rigor
386
396
 
387
397
  body.is_a?(Prism::Node) && scan(body, scope, base: true)
388
398
  end
389
- private_class_method :entry_may_match?
399
+ private_class_method :frame_entry, :entry_may_match?
390
400
  end
391
401
  end
392
402
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-127 WD3 — the callee floor's memo audit. A memo hit is correct only when the key holds every input the value
6
+ # was computed from, and a key that misses one answers a caller with another caller's value (#2516, #2524). With the
7
+ # audit on, every hit in the callee floor's memos (`resolved_census`, the unrecorded hand-off memo, the successor
8
+ # store and `def_census`) recomputes the value and raises {Mismatch} when it differs from the stored one, so a key
9
+ # defect fails on the first hit instead of waiting for an order that exposes it.
10
+ #
11
+ # Like {BudgetTrace}, a switch for specs and CI (`RIGOR_MEMO_AUDIT=1` in the environment, read once at load, or
12
+ # {enable!}): off by default, a single boolean check when off. A recomputation
13
+ # runs with the audit suspended on its thread and the memos bypassed ({suspended?}), so it reads nothing stored.
14
+ module MemoAudit
15
+ # A memo hit whose stored value differs from a fresh computation.
16
+ class Mismatch < StandardError; end
17
+
18
+ SUSPENDED_KEY = :rigor_memo_audit_suspended
19
+
20
+ @enabled = %w[1 true].include?(ENV["RIGOR_MEMO_AUDIT"].to_s.downcase)
21
+
22
+ module_function
23
+
24
+ def enabled? = @enabled
25
+
26
+ # True while an audit recomputation runs on this thread. The callee floor's memos are then bypassed (read and
27
+ # write), so the recomputation reads no stored entry of another memo: a defect shared across a table (a stale
28
+ # `resolved_census(:edges)` entry) would otherwise make the recomputed value match the stored one.
29
+ def suspended? = Thread.current[SUSPENDED_KEY]
30
+
31
+ def enable! = (@enabled = true)
32
+
33
+ def disable! = (@enabled = false)
34
+
35
+ # `stored`, after checking it against `yield` when the audit is on. `label` and the key parts (`key`, `key2`) name
36
+ # the memo entry in the failure; they are passed apart so an audit that is off allocates nothing.
37
+ def check(label, stored, key, key2 = nil, &)
38
+ return stored unless @enabled && !Thread.current[SUSPENDED_KEY]
39
+
40
+ fresh = suspended(&)
41
+ return stored if fresh == stored
42
+
43
+ raise Mismatch, "#{label} memo hit differs from a fresh computation for #{[key, key2].inspect[0, 120]}: " \
44
+ "stored #{stored.inspect[0, 200]}, fresh #{fresh.inspect[0, 200]}"
45
+ end
46
+
47
+ def suspended
48
+ Thread.current[SUSPENDED_KEY] = true
49
+ yield
50
+ ensure
51
+ Thread.current[SUSPENDED_KEY] = nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -7,7 +7,7 @@ module Rigor
7
7
  # Canonical RBS-core aliases shipped by `core/builtin.rbs` whose body is `<Nominal> | _DuckType`.
8
8
  # Matching an overload against an Integer literal should pick the `(int) -> Array[Elem]` body over
9
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
10
+ # The translator reads each alias's interface arm as `Dynamic[Top]` (interfaces are not structurally matched
11
11
  # yet), so a dedicated pass 1.5 between strict and gradual consults this map to pick the alias
12
12
  # whose strict arm matches.
13
13
  #
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # The positional argument lists a call with a precise union argument stands for (#2379): one per combination
9
+ # of the union arguments' members, since the runtime selects an overload per value. `RbsDispatch` selects an
10
+ # overload for each list and joins their returns.
11
+ module ArgumentDistribution
12
+ # The most lists {.lists} spells out when more than one argument is a union. One union argument is linear in
13
+ # its members and is not capped.
14
+ LIMIT = 8
15
+ private_constant :LIMIT
16
+
17
+ # {.lists}' answer past {LIMIT}: the call's overloads cannot be read per value, and it answers `Dynamic[top]`.
18
+ OVER_LIMIT = :over_limit
19
+
20
+ module_function
21
+
22
+ # The lists, nil when no argument is a union, or {OVER_LIMIT}.
23
+ def lists(arg_types)
24
+ unions = arg_types.count { |arg| arg.is_a?(Type::Union) }
25
+ return nil if unions.zero?
26
+
27
+ if unions > 1
28
+ count = arg_types.reduce(1) { |product, arg| arg.is_a?(Type::Union) ? product * arg.members.size : product }
29
+ return OVER_LIMIT if count > LIMIT
30
+ end
31
+
32
+ arg_types.reduce([[]]) do |partials, arg|
33
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
34
+ partials.flat_map { |partial| members.map { |member| partial + [member] } }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -317,7 +317,7 @@ module Rigor
317
317
  end
318
318
 
319
319
  # Returns the receiver's known finite element count, or nil when the carrier does not pin a size.
320
- # Tuple and HashShape are pinned by construction; `Constant<…>` exposes the literal's `.size`. Other
320
+ # Tuple and HashShape are pinned by construction, and a `Constant` Range by its bounds. Other
321
321
  # shapes (Array[T], Range[T], Nominal) decline so the RBS tier widens.
322
322
  def finite_size(receiver)
323
323
  case receiver
@@ -192,8 +192,8 @@ module Rigor
192
192
 
193
193
  # #2115 — the numeric twin of `STRING_FOLD_BYTE_LIMIT`. A folded Integer (and each Integer part of a
194
194
  # folded Rational / Complex) is at most this many bits, the largest bit length whose decimal
195
- # rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds, `2 ** 13_606`
196
- # (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
195
+ # rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds,
196
+ # `2 ** 13_606` (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
197
197
  # rendering it in decimal — which describing, sorting and printing the `Constant` does — takes
198
198
  # minutes. `**` / `pow` / `<<` / `>>` also check their operands before invoking, so the big value
199
199
  # is never computed.
@@ -219,6 +219,15 @@ module Rigor
219
219
  # declared `Numeric`, on which `(2 ** 20_000).even?` is an undefined method.
220
220
  WidenedResult = Data.define(:class_name)
221
221
 
222
+ # #2125 — a String result past `STRING_FOLD_BYTE_LIMIT`, or an operand that would build one. Unlike a
223
+ # `WidenedResult` it declines the WHOLE fold, every other member of a union receiver included: dropping
224
+ # only the oversized member would answer the rest (`(c ? "a" : big) * 2` read `"aa"`). The next tier then
225
+ # answers the call — `LiteralStringFolding`'s `literal-string`, or RBS's `String`, never less than the
226
+ # `Nominal[String]` the fold could have widened to.
227
+ OversizedString = Data.define
228
+ OVERSIZED_STRING = [OversizedString.new].freeze
229
+ private_constant :OversizedString, :OVERSIZED_STRING
230
+
222
231
  # Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
223
232
  # both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
224
233
  # (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
@@ -331,6 +340,7 @@ module Rigor
331
340
  arg = args.first
332
341
  ruby_arg = format_argument_value(arg)
333
342
  return nil if ruby_arg.nil?
343
+ return nil if format_blow_up?(receiver.value, ruby_arg)
334
344
 
335
345
  result = receiver.value % ruby_arg
336
346
  return nil unless foldable_constant_value?(result)
@@ -370,7 +380,7 @@ module Rigor
370
380
  # Normalises an input type into one of:
371
381
  # - `Array<Object>` for a `Constant` (1-element) or `Union[Constant…]` (n-element) — concrete values
372
382
  # to enumerate.
373
- # - `Type::IntegerRange` — bounded interval.
383
+ # - `Type::IntegerRange` / `Type::FloatRange` — bounded interval.
374
384
  # - `nil` — the input shape is not foldable.
375
385
  def numeric_set_of(type)
376
386
  case type
@@ -1200,6 +1210,8 @@ module Rigor
1200
1210
  # "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
1201
1211
  # arithmetic over integers).
1202
1212
  def build_constant_type(values, source: nil)
1213
+ return nil if values.any?(OversizedString)
1214
+
1203
1215
  widened, values = values.partition { |value| value.is_a?(WidenedResult) }
1204
1216
  return build_scalar_constant_type(values, source) if widened.empty?
1205
1217
 
@@ -1213,7 +1225,8 @@ module Rigor
1213
1225
  def build_scalar_constant_type(values, source)
1214
1226
  return nil if values.empty?
1215
1227
 
1216
- unique = values.uniq
1228
+ # #1513 — keyed so the possible results `0.0` and `-0.0` stay two members, as `Constant#==` keeps them.
1229
+ unique = values.uniq { |value| Type::Constant.value_key(value) }
1217
1230
  return collapse_constants(unique) if unique.size <= UNION_FOLD_OUTPUT_LIMIT
1218
1231
 
1219
1232
  widen_to_integer_range(unique, source) ||
@@ -1643,6 +1656,7 @@ module Rigor
1643
1656
  # `flat_map`.
1644
1657
  def invoke_binary(receiver_value, method_name, arg_value)
1645
1658
  return nil unless safe?(receiver_value, method_name, arg_value)
1659
+ return OVERSIZED_STRING if string_blow_up?(receiver_value, method_name, arg_value)
1646
1660
  return nil if range_enumeration_blow_up?(receiver_value, method_name)
1647
1661
 
1648
1662
  widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
@@ -1658,6 +1672,7 @@ module Rigor
1658
1672
  # `false`/`nil` folds.
1659
1673
  def invoke_ternary(receiver_value, method_name, av0, av1)
1660
1674
  return nil unless ternary_method_allowed?(receiver_value, method_name)
1675
+ return OVERSIZED_STRING if string_ternary_blow_up?(receiver_value, method_name, av0, av1)
1661
1676
 
1662
1677
  fold_result(receiver_value.public_send(method_name, av0, av1))
1663
1678
  rescue StandardError
@@ -1667,7 +1682,7 @@ module Rigor
1667
1682
  # Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
1668
1683
  def invoke_unary(receiver_value, method_name)
1669
1684
  return nil unless unary_safe?(receiver_value, method_name)
1670
- return nil if string_unary_blow_up?(receiver_value, method_name)
1685
+ return OVERSIZED_STRING if string_unary_blow_up?(receiver_value, method_name)
1671
1686
  return nil if range_enumeration_blow_up?(receiver_value, method_name)
1672
1687
 
1673
1688
  fold_result(receiver_value.public_send(method_name))
@@ -1675,10 +1690,11 @@ module Rigor
1675
1690
  nil
1676
1691
  end
1677
1692
 
1678
- # `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`, nil
1679
- # for anything else.
1693
+ # `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`,
1694
+ # `OVERSIZED_STRING` for a String past `STRING_FOLD_BYTE_LIMIT`, nil for anything else.
1680
1695
  def fold_result(result)
1681
1696
  return [WidenedResult.new(result.class.name)] if oversized_number?(result)
1697
+ return OVERSIZED_STRING if oversized_string?(result)
1682
1698
 
1683
1699
  foldable_constant_value?(result) ? [result] : nil
1684
1700
  end
@@ -1822,10 +1838,20 @@ module Rigor
1822
1838
  return false if value.is_a?(Float) && value.nan?
1823
1839
  return false if value.is_a?(Complex) && complex_nan?(value)
1824
1840
  return false if oversized_number?(value)
1841
+ return false if oversized_string?(value)
1825
1842
 
1826
1843
  true
1827
1844
  end
1828
1845
 
1846
+ # #2125 — the result gate every String-producing fold passes through, the String twin of
1847
+ # `oversized_number?`. The operand prechecks (`string_blow_up?`, `string_ternary_blow_up?`,
1848
+ # `format_blow_up?`) keep the costly cases from being built at all; this catches the rest after the
1849
+ # fact (`tr` into multibyte characters, `dump`, `Integer#to_s(2)`, whose receiver
1850
+ # `INTEGER_FOLD_BIT_LIMIT` already bounds, so its rendering is at most 13 KB).
1851
+ def oversized_string?(value)
1852
+ value.is_a?(String) && value.bytesize > STRING_FOLD_BYTE_LIMIT
1853
+ end
1854
+
1829
1855
  # True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
1830
1856
  def oversized_number?(value)
1831
1857
  case value
@@ -1874,7 +1900,6 @@ module Rigor
1874
1900
  catalog_allows?(receiver_value, method_name)
1875
1901
  return false unless allowed
1876
1902
  return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
1877
- return false if string_blow_up?(receiver_value, method_name, arg_value)
1878
1903
 
1879
1904
  true
1880
1905
  end
@@ -1960,16 +1985,91 @@ module Rigor
1960
1985
  case method_name
1961
1986
  when :+ then string_concat_blow_up?(receiver_value, arg_value)
1962
1987
  when :* then string_repeat_blow_up?(receiver_value, arg_value)
1988
+ when :% then format_blow_up?(receiver_value, arg_value)
1963
1989
  when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
1964
1990
  else false
1965
1991
  end
1966
1992
  end
1967
1993
 
1994
+ # #2125 — the two-argument String folds that can grow past the input: the padded `center` / `ljust`
1995
+ # / `rjust` (width, pad) and `sub` / `gsub` (pattern, replacement).
1996
+ def string_ternary_blow_up?(receiver_value, method_name, av0, av1)
1997
+ return false unless receiver_value.is_a?(String)
1998
+
1999
+ case method_name
2000
+ when :center, :ljust, :rjust then string_pad_blow_up?(av0)
2001
+ when :sub, :gsub then string_substitution_blow_up?(receiver_value, method_name, av1)
2002
+ else false
2003
+ end
2004
+ end
2005
+
2006
+ # An upper bound on a `sub` / `gsub` result with a String replacement: `gsub` matches at most once per
2007
+ # position plus once at the end, and each replacement is the replacement's bytes plus, for every
2008
+ # backslash in it (`\0`, `\1`, `\k<name>`, `` \` ``, `\'`), at most the receiver's bytes again.
2009
+ def string_substitution_blow_up?(receiver_value, method_name, replacement)
2010
+ return false unless replacement.is_a?(String)
2011
+
2012
+ size = receiver_value.bytesize
2013
+ matches = method_name == :gsub ? size + 1 : 1
2014
+ per_match = replacement.bytesize + (replacement.count("\\") * size)
2015
+ size + (matches * per_match) > STRING_FOLD_BYTE_LIMIT
2016
+ end
2017
+
2018
+ # #2125 — `String#%` / `Kernel#format` pad to a field width and render to a precision the format
2019
+ # string spells (`"%02000000000d"`, `"%.100000f"`) or an argument supplies (`"%*d" % [n, 1]`).
2020
+ # Formatting builds the whole padded string before any result gate can look at it — two billion
2021
+ # bytes took 13 s and 4 GB — so the numbers are read from the directives first. A `*` width counts
2022
+ # every Integer argument as a possible width, and a `%%` escape matches as a whole, carrying no digits.
2023
+ FORMAT_DIRECTIVE = /%%|%(?:<[^>]*>|\{[^}]*\}|[-+ 0#*$.\d])*/
2024
+ private_constant :FORMAT_DIRECTIVE
2025
+
2026
+ def format_blow_up?(format_string, arguments)
2027
+ star = false
2028
+ format_string.scan(FORMAT_DIRECTIVE) do |directive|
2029
+ spec = directive.gsub(/<[^>]*>|\{[^}]*\}|\d+\$/, "")
2030
+ return true if spec.scan(/\d+/).any? { |digits| digits.to_i > STRING_FOLD_BYTE_LIMIT }
2031
+
2032
+ star ||= spec.include?("*")
2033
+ end
2034
+ star && format_argument_values(arguments).any? do |value|
2035
+ width = integer_operand(value)
2036
+ !width.nil? && width.abs > STRING_FOLD_BYTE_LIMIT
2037
+ end
2038
+ end
2039
+
2040
+ def format_argument_values(arguments)
2041
+ case arguments
2042
+ when Hash then arguments.values
2043
+ when Array then arguments
2044
+ else [arguments]
2045
+ end
2046
+ end
2047
+
1968
2048
  # `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
1969
2049
  # literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
1970
2050
  # byte limit the concat / repeat paths use.
1971
2051
  def string_pad_blow_up?(arg_value)
1972
- arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
2052
+ width = integer_operand(arg_value)
2053
+ !width.nil? && width > STRING_FOLD_BYTE_LIMIT
2054
+ end
2055
+
2056
+ # The Integer a width or count operand stands for. Ruby converts a Float or Rational one with `NUM2LONG`
2057
+ # (`"x" * 2.9` is `"xx"`, `"x".ljust(2e9)` pads to two billion), and a Complex one whose imaginary part is an
2058
+ # exact zero through its real part (`"x" * Complex(3, 0)` is `"xxx"`; `Complex(3, 0.0)` raises), so the
2059
+ # prechecks read it the same way. Those are every Numeric class a `Constant` carries. A non-finite Float
2060
+ # raises in Ruby, and any other operand is left to the call, so both answer nil.
2061
+ def integer_operand(value)
2062
+ case value
2063
+ when Integer then value
2064
+ when Float then value.finite? ? value.to_i : nil
2065
+ when Rational then value.to_i
2066
+ when Complex then exact_real_operand(value)
2067
+ end
2068
+ end
2069
+
2070
+ def exact_real_operand(value)
2071
+ imaginary = value.imaginary
2072
+ imaginary.is_a?(Integer) && imaginary.zero? ? integer_operand(value.real) : nil
1973
2073
  end
1974
2074
 
1975
2075
  def string_concat_blow_up?(receiver_value, arg_value)
@@ -1978,10 +2078,11 @@ module Rigor
1978
2078
  end
1979
2079
 
1980
2080
  def string_repeat_blow_up?(receiver_value, arg_value)
1981
- return false unless arg_value.is_a?(Integer)
1982
- return true if arg_value.negative?
2081
+ # A negative count raises (`ArgumentError`), so the call itself skips that member, as any raising one is.
2082
+ count = integer_operand(arg_value)
2083
+ return false if count.nil? || count.negative?
1983
2084
 
1984
- receiver_value.bytesize * arg_value > STRING_FOLD_BYTE_LIMIT
2085
+ receiver_value.bytesize * count > STRING_FOLD_BYTE_LIMIT
1985
2086
  end
1986
2087
  end
1987
2088
  end