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,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../run_scoped_slots"
6
+ require_relative "closure_escape_analyzer"
7
+ require_relative "construction_escape"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2608, review rounds 1 and 2 of #2621 — which locals of a body another holder may write, so a remover on
12
+ # them is not narrowed ({RemoverNarrowing}): a narrowing is a fact about the object, and a write through any other
13
+ # holder after the call would outlive it. One flow-insensitive scan per frame body; a local is a holder's when:
14
+ #
15
+ # - a closure that may run later references it: a lambda, or a block passed to a call that is not a catalogued
16
+ # iterator or `tap` / `then` / `yield_self` (`proc { l << 1 }`), created before or after the remover;
17
+ # - it is read anywhere inside the value of any write (local, instance, class, global or constant variable, an
18
+ # index or attribute write, an operator or `||=` / `&&=` write, a multiple assignment) or inside the subject of a
19
+ # `case`/`in`, `=>` or `in` pattern: `al = c ? l : []`, `al = l.push(1)`, `@k = l`, `case l; in al`. The local
20
+ # such a write or pattern binds is a holder too (both names of a copy), unless the write's value is a fresh
21
+ # object: an Array or Hash literal, or `new` / `[]` on a bare or top-level `Array`, `Hash` or `Set`
22
+ # (`a = Array.new(n, "a")`);
23
+ # - a method's parameter, when its body has a zero-argument `super`, which passes every parameter on;
24
+ # - it is read inside an Array element, a Hash key or value, a splat, or an argument of any call, `yield` or
25
+ # `super` (positional, keyword, splat or block-pass), unless the read is the receiver of a call that answers
26
+ # a value other than the receiver and keeps no reference to it ({PURE_READERS}: #2555's per-class
27
+ # `ConstructionEscape::READERS`, less the conversions that return self on another class): `puts(l.size)`,
28
+ # `dump_type(l.dup)` and `dump_type(l.first)` hold nothing, `Keep.new(l)` and `foo(l.push(1))` do;
29
+ # - it receives a reflective call (`l.send(...)`, `l.instance_eval { }`, `l.tap { }`);
30
+ # - and every local, when the body calls `binding`, `eval`, `local_variable_get` or `local_variable_set` on any
31
+ # receiver (`-> {}.binding`), or a receiverless `instance_variable_set`.
32
+ #
33
+ # A receiver position, a `return` and a body's last expression hand nothing on.
34
+ module RemoverHolderScan
35
+ REFLECTIVE_CALLS = %i[
36
+ send public_send __send__ instance_eval instance_exec tap then yield_self itself method
37
+ define_singleton_method
38
+ ].to_set.freeze
39
+ # A conversion one class lists as a reader returns the receiver itself on another (`Array#to_a`, `Hash#to_h`,
40
+ # `Set#to_set`), so the union of the per-class lists leaves them out.
41
+ SELF_CONVERSIONS = %i[to_a to_h to_set].freeze
42
+ PURE_READERS = (ConstructionEscape::READERS.values.reduce(:|) - SELF_CONVERSIONS).freeze
43
+ IMMEDIATE_BLOCK_CALLS = %i[tap then yield_self].to_set.freeze
44
+ FRAME_CALLS = %i[binding eval local_variable_get local_variable_set instance_variable_set].to_set.freeze
45
+ # Frame calls that reach the locals whatever their receiver (`pr.binding`, `Kernel.eval`).
46
+ ANY_RECEIVER_FRAME_CALLS = %i[binding eval local_variable_get local_variable_set].to_set.freeze
47
+ NON_WRITER_OPERATORS = %i[== != <= >= ===].to_set.freeze
48
+ CORE_COLLECTIONS = %i[Array Hash Set].to_set.freeze
49
+ LOCAL_REFERENCES = [
50
+ Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode,
51
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
52
+ ].freeze
53
+ LOCAL_WRITES = [
54
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
55
+ Prism::LocalVariableAndWriteNode
56
+ ].freeze
57
+ ALL = :all
58
+ # A run-scoped slot ({RunScopedSlots}): emptied when a run ends, so no verdict outlives the body it read.
59
+ UNSAFE_CACHE_KEY = RunScopedSlots.register(:__rigor_remover_narrowing_unsafe)
60
+ private_constant :REFLECTIVE_CALLS, :SELF_CONVERSIONS, :PURE_READERS, :IMMEDIATE_BLOCK_CALLS, :FRAME_CALLS,
61
+ :ANY_RECEIVER_FRAME_CALLS, :NON_WRITER_OPERATORS, :CORE_COLLECTIONS,
62
+ :LOCAL_REFERENCES, :LOCAL_WRITES, :ALL, :UNSAFE_CACHE_KEY
63
+
64
+ module_function
65
+
66
+ # Whether the local `name` may be written through another holder somewhere in the body `scope` runs. A scope
67
+ # with no known body answers true.
68
+ def unsafe_receiver?(scope, name)
69
+ frame = scope.match_frame
70
+ body = frame&.body
71
+ return true if body.nil?
72
+
73
+ table = (Thread.current[UNSAFE_CACHE_KEY] ||= ObjectSpace::WeakKeyMap.new)
74
+ names = table[body] ||= unsafe_names(body, frame.parameters)
75
+ names.equal?(ALL) || names.include?(name.to_sym)
76
+ end
77
+
78
+ def unsafe_names(body, parameters = nil)
79
+ names = Set.new
80
+ catch(ALL) do
81
+ collect_unsafe(body, false, names)
82
+ parameter_names(parameters, names) if forwards_parameters?(body)
83
+ return names.freeze
84
+ end
85
+ ALL
86
+ end
87
+
88
+ def collect_unsafe(node, in_closure, names)
89
+ in_closure ||= node.is_a?(Prism::LambdaNode)
90
+ names << node.name if in_closure && LOCAL_REFERENCES.any? { |k| node.is_a?(k) }
91
+ record_node(node, names)
92
+
93
+ node.compact_child_nodes.each do |child|
94
+ child_in_closure = in_closure || (child.is_a?(Prism::BlockNode) && deferred_block?(node))
95
+ collect_unsafe(child, child_in_closure, names)
96
+ end
97
+ end
98
+
99
+ def record_node(node, names) # rubocop:disable Metrics/CyclomaticComplexity
100
+ case node
101
+ when Prism::CallNode then record_call(node, names)
102
+ when Prism::YieldNode, Prism::SuperNode then node.arguments&.arguments&.each { |a| record_held(a, names) }
103
+ when Prism::ArrayNode then node.elements.each { |element| record_held(element, names) }
104
+ when Prism::AssocNode then [node.key, node.value].each { |part| record_held(part, names) }
105
+ when Prism::SplatNode, Prism::BlockArgumentNode then record_held(node.expression, names)
106
+ when Prism::CaseMatchNode then record_pattern(node.predicate, node.conditions, names)
107
+ when Prism::MatchRequiredNode, Prism::MatchPredicateNode then record_pattern(node.value, [node.pattern], names)
108
+ when Prism::MultiWriteNode then record_multi_write(node, names)
109
+ else record_write(node, names)
110
+ end
111
+ end
112
+
113
+ def record_call(node, names)
114
+ throw ALL if frame_call?(node)
115
+
116
+ receiver = unparenthesised(node.receiver)
117
+ names << receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && REFLECTIVE_CALLS.include?(node.name)
118
+ arguments = node.arguments&.arguments || []
119
+ if writer_call?(node) && !arguments.empty?
120
+ *keys, value = arguments
121
+ keys.each { |key| record_held(key, names) }
122
+ every_read(value, names)
123
+ else
124
+ arguments.each { |argument| record_held(argument, names) }
125
+ end
126
+ end
127
+
128
+ # A zero-argument `super` passes every parameter of the method on, implicitly. (A `...` forward passes only the
129
+ # anonymous parameters, which name no local.)
130
+ def forwards_parameters?(node)
131
+ node.is_a?(Prism::ForwardingSuperNode) || node.compact_child_nodes.any? { |child| forwards_parameters?(child) }
132
+ end
133
+
134
+ def parameter_names(node, names)
135
+ return if node.nil?
136
+
137
+ names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
138
+ node.compact_child_nodes.each { |child| parameter_names(child, names) }
139
+ end
140
+
141
+ def frame_call?(node)
142
+ ANY_RECEIVER_FRAME_CALLS.include?(node.name) || (node.receiver.nil? && FRAME_CALLS.include?(node.name))
143
+ end
144
+
145
+ def unparenthesised(node)
146
+ node = node.body.body.first while node.is_a?(Prism::ParenthesesNode) && node.body&.body&.size == 1
147
+ node
148
+ end
149
+
150
+ # An index or attribute write (`h[k] = v`, `o.x = v`, `o&.x = v`): its value is a write's value.
151
+ def writer_call?(node)
152
+ name = node.name
153
+ name.end_with?("=") && !NON_WRITER_OPERATORS.include?(name)
154
+ end
155
+
156
+ # A write's value hands on every local read anywhere inside it, and the local it binds holds what it read.
157
+ def record_write(node, names)
158
+ return unless node.class.name.end_with?("WriteNode") && node.respond_to?(:value)
159
+ return unless every_read(node.value, names)
160
+
161
+ names << node.name if LOCAL_WRITES.any? { |k| node.is_a?(k) } && !fresh_value?(node.value)
162
+ end
163
+
164
+ # `Array`, `Hash` or `Set`, bare or as a top-level path (`::Set`): a project class may define its own `self.[]` or
165
+ # `self.new` that returns its argument.
166
+ def core_collection_constant?(node)
167
+ top_level = node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)
168
+ top_level && CORE_COLLECTIONS.include?(node.name)
169
+ end
170
+
171
+ # A value that is a new object whatever it reads, so the local it binds aliases nothing.
172
+ def fresh_value?(value)
173
+ case value
174
+ when Prism::ArrayNode, Prism::HashNode then true
175
+ when Prism::CallNode then %i[new []].include?(value.name) && core_collection_constant?(value.receiver)
176
+ else false
177
+ end
178
+ end
179
+
180
+ def record_multi_write(node, names)
181
+ return unless every_read(node.value, names)
182
+
183
+ local_targets([*node.lefts, node.rest, *node.rights], names)
184
+ end
185
+
186
+ def record_pattern(subject, patterns, names)
187
+ return unless every_read(subject, names)
188
+
189
+ local_targets(patterns, names)
190
+ end
191
+
192
+ # Adds every local `node` reads, at any depth; answers whether it read any.
193
+ def every_read(node, names)
194
+ return false if node.nil?
195
+
196
+ found = node.is_a?(Prism::LocalVariableReadNode)
197
+ names << node.name if found
198
+ node.compact_child_nodes.reduce(found) { |acc, child| every_read(child, names) || acc }
199
+ end
200
+
201
+ def local_targets(nodes, names)
202
+ nodes.compact.each do |node|
203
+ names << node.name if node.is_a?(Prism::LocalVariableTargetNode)
204
+ local_targets(node.compact_child_nodes, names)
205
+ end
206
+ end
207
+
208
+ # A held position: every local read inside it but the receiver of a {PURE_READERS} call.
209
+ def record_held(node, names)
210
+ return if node.nil?
211
+ return if pure_read?(node)
212
+
213
+ names << node.name if node.is_a?(Prism::LocalVariableReadNode)
214
+ node.compact_child_nodes.each { |child| record_held(child, names) }
215
+ end
216
+
217
+ # A {PURE_READERS} call on a local hands the local on to nothing. Its own arguments are held positions of the
218
+ # call itself, which the body walk records when it reaches that call.
219
+ def pure_read?(node)
220
+ node.is_a?(Prism::CallNode) && PURE_READERS.include?(node.name) &&
221
+ node.receiver.is_a?(Prism::LocalVariableReadNode)
222
+ end
223
+
224
+ # A block that may run after its call returns: any but a catalogued iterator's, or `tap` / `then` /
225
+ # `yield_self`'s.
226
+ def deferred_block?(call_node)
227
+ return true unless call_node.is_a?(Prism::CallNode)
228
+
229
+ !(ClosureEscapeAnalyzer.iterator_name?(call_node.name) || IMMEDIATE_BLOCK_CALLS.include?(call_node.name))
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../type"
6
+ require_relative "remover_holder_scan"
7
+ # `Narrowing` is resolved at call time: requiring it here closes a load cycle through `KeyPresenceGuard`.
8
+
9
+ module Rigor
10
+ module Inference
11
+ # Issue #2608 — a collection remover or filter narrows its receiver's element (or Hash key / value) type by what
12
+ # Ruby guarantees it removed, where that is provable from the call alone:
13
+ #
14
+ # - `Array#compact!` / `Hash#compact!` drop `nil` from the element / value;
15
+ # - `Array#delete(nil)` / `Set#delete(nil)` / `Set#delete?(nil)` drop `nil` from the element, and `Hash#delete(nil)`
16
+ # drops it from the key, given a literal `nil`;
17
+ # - `reject!` / `delete_if` keep the elements the block answers falsey for, and `select!` / `filter!` / `keep_if`
18
+ # the ones it answers truthy for. The block's one expression is read by the engine's own predicate narrowing
19
+ # ({Narrowing.predicate_scopes}) with its parameter bound to the element (the Hash `|k, v|` form binds both), and
20
+ # the surviving element is that parameter's binding on the matching edge. `&:nil?` reads as `{ |x| x.nil? }`;
21
+ # - a closed `HashShape` loses the key a literal `delete(:k)` names.
22
+ #
23
+ # Everything else (`pop`, `shift`, `slice!`, `subtract`, `delete` of a non-literal, a block whose predicate the
24
+ # narrowing does not read) answers `nil`: which members remain is not provable, and the caller keeps its reading.
25
+ #
26
+ # Only a receiver that is itself a local read narrows, and only when no other holder in its body may write the
27
+ # object ({RemoverHolderScan}): the narrowing is a fact about the object. A receiver selecting among bindings
28
+ # (`(f ? a : b).compact!`) mutates one of them, and narrowing every candidate would drop a member from the one the
29
+ # call never touched. An instance variable never narrows: any method, or any object that holds it, can write it.
30
+ module RemoverNarrowing
31
+ NIL_REJECTORS = %i[compact!].to_set.freeze
32
+ NIL_LITERAL_DELETERS = %i[delete delete?].to_set.freeze
33
+ FALSEY_KEEPERS = %i[reject! delete_if].to_set.freeze
34
+ TRUTHY_KEEPERS = %i[select! filter! keep_if].to_set.freeze
35
+ CANDIDATES = (NIL_REJECTORS | NIL_LITERAL_DELETERS | FALSEY_KEEPERS | TRUTHY_KEEPERS).freeze
36
+ NARROWING_RECEIVERS = [Prism::LocalVariableReadNode].freeze
37
+ CARRIERS = %w[Array Hash Set].freeze
38
+ # Nodes that make a one-expression block's value something other than its predicate, or rebind a parameter.
39
+ OPAQUE_BLOCK_NODES = [
40
+ Prism::NextNode, Prism::BreakNode, Prism::ReturnNode, Prism::RedoNode, Prism::RetryNode, Prism::YieldNode,
41
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
42
+ Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
43
+ ].freeze
44
+ private_constant :NIL_REJECTORS, :NIL_LITERAL_DELETERS, :FALSEY_KEEPERS, :TRUTHY_KEEPERS, :NARROWING_RECEIVERS,
45
+ :CARRIERS, :OPAQUE_BLOCK_NODES
46
+
47
+ module_function
48
+
49
+ # Whether `call_node` may narrow its receiver at all: a cheap syntactic gate for {.narrow}.
50
+ def candidate?(call_node)
51
+ CANDIDATES.include?(call_node.name) && NARROWING_RECEIVERS.any? { |klass| call_node.receiver.is_a?(klass) }
52
+ end
53
+
54
+ # The closed `HashShape` `shape` without the key a literal `delete(:k)` names, or `nil` when the call is no such
55
+ # delete or the shape is open (an unseen key may be the one deleted, and the opened default reads it back).
56
+ def narrow_shape(shape, call_node)
57
+ return nil unless shape.is_a?(Type::HashShape) && shape.closed? && call_node.name == :delete
58
+
59
+ key = literal_key(single_argument(call_node))
60
+ return nil if key.equal?(NO_KEY)
61
+ return shape unless shape.pairs.key?(key)
62
+ return nil if shape.read_only_key?(key)
63
+
64
+ Type::Combinator.hash_shape_of(
65
+ shape.pairs.except(key),
66
+ required_keys: shape.required_keys - [key], optional_keys: shape.optional_keys - [key],
67
+ read_only_keys: shape.read_only_keys, extra_keys: :closed
68
+ )
69
+ end
70
+
71
+ # `type` with each Array / Hash / Set member narrowed by what `call_node` removes, member-wise through a union.
72
+ # `nil` when the call narrows no member (it is no exact remover for the member's class, or its block predicate
73
+ # says nothing about the element). A member the call answers for without change (`compact!` on `Array[Integer]`)
74
+ # still counts as answered: the caller then keeps the precise reading rather than a floored one.
75
+ def narrow(type, call_node, scope)
76
+ answered = false
77
+ members = members_of(type).map do |member|
78
+ narrowed = narrow_member(member, call_node, scope)
79
+ next member if narrowed.nil?
80
+
81
+ answered = true
82
+ narrowed
83
+ end
84
+ answered ? Type::Combinator.union(*members) : nil
85
+ end
86
+
87
+ def members_of(type)
88
+ type.is_a?(Type::Union) ? type.members : [type]
89
+ end
90
+
91
+ def narrow_member(member, call_node, scope)
92
+ return nil unless member.is_a?(Type::Nominal) && CARRIERS.include?(member.class_name)
93
+
94
+ args = member.type_args
95
+ case member.class_name
96
+ when "Hash" then args.size == 2 ? narrow_hash(member, call_node, scope) : nil
97
+ else args.size == 1 ? narrow_elements(member, call_node, scope) : nil
98
+ end
99
+ end
100
+
101
+ def narrow_elements(member, call_node, scope)
102
+ element = member.type_args.first
103
+ narrowed =
104
+ case call_node.name
105
+ when :compact! then Narrowing.narrow_non_nil(element)
106
+ when :delete, :delete? then nil_argument?(call_node) ? Narrowing.narrow_non_nil(element) : nil
107
+ else block_narrowed(call_node, scope, [element])&.first
108
+ end
109
+ usable?(narrowed) ? Type::Combinator.nominal_of(member.class_name, type_args: [narrowed]) : nil
110
+ end
111
+
112
+ def narrow_hash(member, call_node, scope)
113
+ key, value = member.type_args
114
+ narrowed =
115
+ case call_node.name
116
+ when :compact! then [key, Narrowing.narrow_non_nil(value)]
117
+ when :delete then nil_argument?(call_node) ? [Narrowing.narrow_non_nil(key), value] : nil
118
+ when :delete? then nil
119
+ else block_narrowed(call_node, scope, [key, value])
120
+ end
121
+ narrowed&.all? { |arg| usable?(arg) } ? Type::Combinator.nominal_of("Hash", type_args: narrowed) : nil
122
+ end
123
+
124
+ # A parameter narrowed to `bot` (`select! { |x| x.nil? }` on `Array[Integer]`) says the carrier is empty; that
125
+ # is left to the caller's reading rather than spelled as `Array[bot]`.
126
+ def usable?(type)
127
+ !type.nil? && !type.is_a?(Type::Bot)
128
+ end
129
+
130
+ def nil_argument?(call_node)
131
+ single_argument(call_node).is_a?(Prism::NilNode)
132
+ end
133
+
134
+ def single_argument(call_node)
135
+ arguments = call_node.arguments&.arguments
136
+ arguments&.size == 1 ? arguments.first : nil
137
+ end
138
+
139
+ NO_KEY = Object.new.freeze
140
+ private_constant :NO_KEY
141
+
142
+ def literal_key(node)
143
+ case node
144
+ when Prism::SymbolNode then node.unescaped.to_sym
145
+ else NO_KEY
146
+ end
147
+ end
148
+
149
+ # The block parameters' types on the edge that keeps an element, or `nil` when the call has no block the
150
+ # narrowing reads: one expression, no jump or local write, explicit required parameters matching the carrier
151
+ # (`|x|` for an element, `|k, v|` for a Hash pair), and a predicate that narrows one of them on some edge.
152
+ def block_narrowed(call_node, scope, types)
153
+ keep = keep_edge(call_node.name)
154
+ return nil if keep.nil?
155
+
156
+ block = call_node.block
157
+ return symbol_nil_narrowed(block, keep, types) if block.is_a?(Prism::BlockArgumentNode)
158
+
159
+ names = block_parameter_names(block, types.size)
160
+ predicate = names && block_predicate(block)
161
+ return nil if predicate.nil?
162
+
163
+ predicate_narrowed(predicate, scope, names, types, keep)
164
+ end
165
+
166
+ def keep_edge(method_name)
167
+ if FALSEY_KEEPERS.include?(method_name) then :falsey
168
+ elsif TRUTHY_KEEPERS.include?(method_name) then :truthy
169
+ end
170
+ end
171
+
172
+ def symbol_nil_narrowed(block, keep, types)
173
+ symbol = block.expression
174
+ return nil unless types.size == 1 && symbol.is_a?(Prism::SymbolNode) && symbol.unescaped == "nil?"
175
+
176
+ [keep == :falsey ? Narrowing.narrow_non_nil(types.first) : Narrowing.narrow_nil(types.first)]
177
+ end
178
+
179
+ def block_parameter_names(block, arity)
180
+ return nil unless block.is_a?(Prism::BlockNode)
181
+
182
+ parameters = block.parameters
183
+ return nil unless parameters.is_a?(Prism::BlockParametersNode)
184
+
185
+ list = parameters.parameters
186
+ return nil if list.nil? || !simple_parameters?(list) || list.requireds.size != arity
187
+ return nil unless list.requireds.all?(Prism::RequiredParameterNode)
188
+
189
+ list.requireds.map(&:name)
190
+ end
191
+
192
+ def simple_parameters?(list)
193
+ list.optionals.empty? && list.rest.nil? && list.posts.empty? && list.keywords.empty? &&
194
+ list.keyword_rest.nil? && list.block.nil?
195
+ end
196
+
197
+ def block_predicate(block)
198
+ body = block.body
199
+ return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
200
+
201
+ statement = body.body.first
202
+ opaque?(statement) ? nil : statement
203
+ end
204
+
205
+ def opaque?(node)
206
+ return true if OPAQUE_BLOCK_NODES.any? { |klass| node.is_a?(klass) }
207
+
208
+ node.compact_child_nodes.any? { |child| opaque?(child) }
209
+ end
210
+
211
+ def predicate_narrowed(predicate, scope, names, types, keep)
212
+ bound = names.zip(types).reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
213
+ truthy, falsey = Narrowing.predicate_scopes(predicate, bound)
214
+ read = ->(edge) { names.map { |name| edge.local(name) || bound.local(name) } }
215
+ entry_types = read.call(bound)
216
+ truthy_types = read.call(truthy)
217
+ falsey_types = read.call(falsey)
218
+ return nil if truthy_types == entry_types && falsey_types == entry_types
219
+
220
+ keep == :truthy ? truthy_types : falsey_types
221
+ end
222
+ end
223
+ end
224
+ end
@@ -24,7 +24,8 @@ module Rigor
24
24
  # replacement, and the seed it would otherwise keep beside it is gone.
25
25
  REPLACED = Ractor.make_shareable({
26
26
  "Array" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] },
27
- "Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] }
27
+ "Hash" => { transform_keys!: [0], transform_values!: [1], replace: [0, 1] },
28
+ "Set" => { map!: [0], collect!: [0], flatten!: [0], replace: [0] }
28
29
  })
29
30
 
30
31
  # Mutators that store values nothing describes BESIDE values they may keep: `fill(x, start)` leaves the slots
@@ -66,6 +67,56 @@ module Rigor
66
67
  Type::Combinator.nominal_of(carrier.class_name, type_args: args)
67
68
  end
68
69
 
70
+ # The element rewriters {.typed} answers for, keyed to the non-bang call whose result is the rewritten
71
+ # receiver: `a.map!(&:to_i)` holds what `a.map(&:to_i)` returns, and `a.flatten!` what `a.flatten` does.
72
+ TYPED_COUNTERPARTS = Ractor.make_shareable({ map!: :map, collect!: :collect, flatten!: :flatten })
73
+
74
+ # The carrier classes {.typed} rewrites, with the rewriters each takes. `Set#flatten!` is absent: it flattens
75
+ # only nested Sets, so `ARGV.map { |x| [x] }.to_set.flatten!` still holds `[String]` Arrays, while `Set#flatten`
76
+ # answers an element its RBS does not narrow.
77
+ TYPED_CARRIERS = Ractor.make_shareable({ "Array" => %i[map! collect! flatten!], "Set" => %i[map! collect!] })
78
+
79
+ # Issue #2427 — `carrier` with its element replaced by `element`, the element of what the non-bang counterpart
80
+ # ({TYPED_COUNTERPARTS}) answers for the same call: `a = ARGV.dup; a.map!(&:to_i)` reads `Array[Integer]` and
81
+ # `a.map!(&:strip)` keeps `Array[String]`. It is the typed form of {.arm}'s {REPLACED} answer, and it replaces
82
+ # the gradual and the precise element alike, since nothing the seed held survives the rewrite.
83
+ #
84
+ # Answers for an `Array` / `Set` nominal and an empty-witness refinement of one (keeping the witness only where
85
+ # {RefinementMutation.preserves_witness?} does: `map!` keeps it, `flatten!` can empty `[[]]`). `nil` for any
86
+ # other carrier or name, so the caller takes the gradual arm instead.
87
+ def typed(carrier, method_name, element)
88
+ return nil unless TYPED_COUNTERPARTS.key?(method_name)
89
+
90
+ case carrier
91
+ when Type::Nominal
92
+ return nil unless carrier.type_args.size == 1 && TYPED_CARRIERS[carrier.class_name]&.include?(method_name)
93
+
94
+ Type::Combinator.nominal_of(carrier.class_name, type_args: [element])
95
+ when Type::Difference
96
+ typed_refinement(carrier, method_name, element)
97
+ end
98
+ end
99
+
100
+ def typed_refinement(difference, method_name, element)
101
+ base = difference.base
102
+ return nil unless difference.removes_empty_witness? && base.is_a?(Type::Nominal)
103
+
104
+ rewritten = typed(base, method_name, element)
105
+ return nil if rewritten.nil?
106
+
107
+ kept = RefinementMutation.preserves_witness?(base.class_name, method_name)
108
+ kept ? Type::Combinator.difference(rewritten, difference.removed) : rewritten
109
+ end
110
+
111
+ # The element of `result`, the non-bang counterpart's answer: an `Array[R]` / `Set[R]` nominal, or an
112
+ # empty-witness refinement of one. `nil` when the answer has no single element to read.
113
+ def counterpart_element(result)
114
+ result = result.base if result.is_a?(Type::Difference) && result.removes_empty_witness?
115
+ return nil unless result.is_a?(Type::Nominal) && %w[Array Set].include?(result.class_name)
116
+
117
+ result.type_args.size == 1 ? result.type_args.first : nil
118
+ end
119
+
69
120
  # {.arm} through a `Union` (member by member) and an empty-witness refinement (its base, keeping the witness
70
121
  # only when the mutator cannot empty the receiver — {RefinementMutation.preserves_witness?}). For a carrier
71
122
  # the ADR-56 slice-C join rebuilt from its seed, which knows the adders' content but not the rewrite.
@@ -114,7 +165,7 @@ module Rigor
114
165
  when Type::Nominal then type.class_name
115
166
  end
116
167
  end
117
- private_class_method :value_carrier_class, :arm_refinement
168
+ private_class_method :value_carrier_class, :arm_refinement, :typed_refinement
118
169
  end
119
170
  end
120
171
  end
@@ -66,6 +66,15 @@ module Rigor
66
66
  classifier.result
67
67
  end
68
68
 
69
+ # Issue #2021 — every contribution of the tree under `root`, for a file discovery reached only through a
70
+ # `require_relative` walk: another file the walk does not see may redefine what it defines, so none of its
71
+ # contributions is certain.
72
+ def every_contribution(root)
73
+ classifier = Classifier.new
74
+ classifier.possible(root)
75
+ classifier.result
76
+ end
77
+
69
78
  # Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
70
79
  def possible?(possible, node)
71
80
  !possible.empty? && possible.include?(node)
@@ -102,7 +111,6 @@ module Rigor
102
111
  node.rigor_each_child { |child| certain(child) }
103
112
  end
104
113
 
105
- # Marks every contribution of the subtree.
106
114
  def possible(node)
107
115
  return unless node.is_a?(Prism::Node)
108
116
 
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_walker"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module ScopeIndexer
10
+ # Issue #2291 — the class-ivar census types an `@x = LIT` write from the type the constant's own write binds, the
11
+ # same way a method body reads `LIT`. The census runs before the file's in-source constant table is seeded, so
12
+ # without it the write typed under no constant table and a reader over the ivar (`def q = @q`) read `Dynamic[top]`
13
+ # where `def g = LIT` read the constant's type.
14
+ #
15
+ # One instance serves one census walk. The file's constant table is built on the first constant-valued write, so
16
+ # a file with none pays nothing; it is built as {ScopeIndexer#index} builds it later (typed under the census's
17
+ # scope, merged over the scope's project seed, a mutated literal widened), and resolved through the ordinary
18
+ # constant lookup. {#type_of} answers nil, leaving the write's ordinary typing, when:
19
+ #
20
+ # - the value is not a constant read (`LIT`, `A::LIT`, `::LIT`); a path over anything else (`self.class::LIT`)
21
+ # resolves to nothing, as it does in a method body;
22
+ # - the file may rebind a constant of that last segment: a second write in any form, an operator, `||=` or `&&=`
23
+ # write, a multiple-assignment target, or a `const_set` / `remove_const` naming it — or naming anything but a
24
+ # literal, which may be it.
25
+ #
26
+ # A constant another file assigns resolves only through the scope's own project seed, as it did before.
27
+ class ConstantIvarValue
28
+ WRITE_NODES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode].freeze
29
+ REBIND_NODES = [Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
30
+ Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode,
31
+ Prism::ConstantPathAndWriteNode, Prism::ConstantTargetNode,
32
+ Prism::ConstantPathTargetNode].freeze
33
+ REBIND_CALLS = %i[const_set remove_const].freeze
34
+ ANY_NAME = :any
35
+ private_constant :WRITE_NODES, :REBIND_NODES, :REBIND_CALLS, :ANY_NAME
36
+
37
+ def initialize(root, scope)
38
+ @root = root
39
+ @scope = scope
40
+ end
41
+
42
+ # The type `value` reads under the file's constant table, or nil when the write keeps its ordinary typing.
43
+ def type_of(value, scope)
44
+ return nil unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
45
+ return nil if rebound?(value.name)
46
+
47
+ scope.with_discovery(scope.discovery.with(in_source_constants: constants)).type_of(value)
48
+ end
49
+
50
+ private
51
+
52
+ def constants
53
+ @constants ||= begin
54
+ literal_mutations = ScopeIndexer.collect_literal_receiver_mutations(@root)
55
+ per_file = ScopeIndexer.widen_mutated_constants(
56
+ ScopeIndexer.build_in_source_constants(@root, @scope), literal_mutations[:constants]
57
+ )
58
+ ScopeIndexer.merge_seeded_constants(@scope.in_source_constants, per_file)
59
+ end
60
+ end
61
+
62
+ def rebound?(name)
63
+ @rebound ||= rebound_names
64
+ @rebound.include?(ANY_NAME) || @rebound.include?(name)
65
+ end
66
+
67
+ def rebound_names
68
+ written = Set.new
69
+ rebound = Set.new
70
+ Source::NodeWalker.each(@root) do |node|
71
+ case node
72
+ when *WRITE_NODES
73
+ name = write_name(node)
74
+ rebound << name unless written.add?(name)
75
+ when *REBIND_NODES then rebound << write_name(node)
76
+ when Prism::CallNode then rebound << call_name(node) if REBIND_CALLS.include?(node.name)
77
+ end
78
+ end
79
+ rebound
80
+ end
81
+
82
+ def write_name(node) = node.respond_to?(:target) ? node.target.name : node.name
83
+
84
+ def call_name(node)
85
+ argument = node.arguments&.arguments&.first
86
+ return argument.unescaped.to_sym if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
87
+
88
+ ANY_NAME
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end