rigortype 0.4.6 → 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 (417) 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 +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../analysis/dependency_recorder"
6
+ require_relative "../source/node_children"
7
+ require_relative "../type"
8
+ require_relative "guard_rebinding"
9
+ require_relative "project_method_ownership"
10
+
11
+ module Rigor
12
+ module Inference
13
+ # Issue #2314 — whether running a node may reach a project method that writes a given global. Both places that
14
+ # end a global's binding at a call ask it: the statement walk past a statement call
15
+ # (`StatementEvaluator#forget_reached_globals`), and a `case` whose global subject a `when` test may rebind
16
+ # (`StatementEvaluator#when_test_rebinds_subject?`, issue #2211), so the two read a global alike.
17
+ #
18
+ # The writers come from {GlobalWriteCensus}: a `[:global_writer, name, method]` entry for each write of `name`
19
+ # inside a method body (`method` is the `def`'s name, or the literal name a `define_method` block defines), and
20
+ # `[:global_writer, name, nil]` for one inside a lambda or a block a call keeps to run later
21
+ # ({StoredBlockCall}), which any call may run. A write at the top level or in a class body runs where it is
22
+ # written, so no call reaches it.
23
+ #
24
+ # {.may_write?} answers true when the global has a writer and:
25
+ #
26
+ # - the node may run code other than Ruby core and the standard library ({GuardRebinding.may_rebind?}, the
27
+ # question #2260 asked of a `when` test), and
28
+ # - some writer is anonymous, or the node's own call has a receiver no target resolves (a `Dynamic` value), or a
29
+ # search by method name reaches a writer ({Search}).
30
+ #
31
+ # The search reads method names, not receivers: from the node it follows every call, by name, into every project
32
+ # `def` of that name on any class, transitively, and reaches a writer when it meets a call named after one. A
33
+ # call naming its method by a computed value (`send(name)`, `eval(code)`) reaches. A name no project `def` defines
34
+ # follows `method_missing`, `new` follows `initialize`, and an alias name (`alias`, `alias_method`, a
35
+ # `def_delegator` alias) follows the name it stands for. A nested `def` is not read, since defining a method runs
36
+ # nothing. Past {Search::DEF_BUDGET} bodies the search answers that it reaches. A `define_method` method that calls
37
+ # a writer is not followed (only its own writes count), and a method core code calls back without the program
38
+ # spelling it (`puts obj` runs `obj.to_s`) is read as the core method alone, as {GuardRebinding} reads it.
39
+ module GlobalWriterReach
40
+ EMPTY = Set.new.freeze
41
+
42
+ module_function
43
+
44
+ # The globals `scope` binds that some project method may write, other than one already `Dynamic[top]`. Each
45
+ # bound global's census key is recorded whatever the answer, so a file that starts writing one re-checks.
46
+ def candidates(scope)
47
+ table = writer_table(scope)
48
+ globals = scope.globals
49
+ globals.each_key { |name| record_writers(name) } if Analysis::DependencyRecorder.active?
50
+ return EMPTY if table.empty? || globals.empty?
51
+
52
+ table.each_key.select do |name|
53
+ type = globals[name]
54
+ !type.nil? && !type.is_a?(Type::Dynamic)
55
+ end
56
+ end
57
+
58
+ # True when running `node`, typed in `scope`, may reach a project method that writes `name` (see the module
59
+ # comment).
60
+ def may_write?(node, name, scope)
61
+ writers = writers(scope, name)
62
+ return false if writers.empty?
63
+ return false unless GuardRebinding.may_rebind?(node, scope)
64
+ return true if writers.include?(nil) || unresolved_call?(node, scope)
65
+
66
+ Search.new(scope, writers).reaches?(node)
67
+ end
68
+
69
+ # The method names that write `name` (nil for an anonymous writer). Records the census key, so a file that
70
+ # starts or stops writing the global re-checks the reader. A guard on the `case` site: every `when` test built so
71
+ # far also records the key through a statement call's {.candidates} or a callee's method edge.
72
+ def writers(scope, name)
73
+ record_writers(name) if Analysis::DependencyRecorder.active?
74
+ writer_table(scope)[name] || EMPTY
75
+ end
76
+
77
+ def record_writers(name)
78
+ Analysis::DependencyRecorder.read_name(:global_census, "global_writer:#{name}")
79
+ end
80
+
81
+ # `{ global => Set[method name or nil] }` over the project's census and the `pre_eval:` files' own.
82
+ def writer_table(scope) = census_tables(scope).first
83
+
84
+ # `{ alias name => Set[the method names it stands for] }`: an `alias`, an `alias_method` or a `def_delegator`
85
+ # with a literal alias name, from the same census.
86
+ def alias_table(scope) = census_tables(scope).last
87
+
88
+ # The two tables above, kept for the census objects they were built from.
89
+ def census_tables(scope)
90
+ census = scope.discovered_global_write_census
91
+ pre_eval = scope.environment&.project_patched_methods&.write_census
92
+ cached = @census_tables
93
+ return cached[2] if cached && cached[0].equal?(census) && cached[1].equal?(pre_eval)
94
+
95
+ tables = build_tables([census, pre_eval])
96
+ @census_tables = [census, pre_eval, tables.freeze]
97
+ tables
98
+ end
99
+
100
+ TABLE_KINDS = { global_writer: 0, method_alias: 1 }.freeze
101
+ private_constant :TABLE_KINDS
102
+
103
+ def build_tables(censuses)
104
+ tables = [{}, {}]
105
+ censuses.each do |entries|
106
+ entries&.each do |kind, key, value|
107
+ index = TABLE_KINDS[kind]
108
+ (tables[index][key] ||= Set.new) << value if index
109
+ end
110
+ end
111
+ tables.each { |table| table.each_value(&:freeze).freeze }
112
+ end
113
+
114
+ # True when `node` is a call whose receiver no target resolves: the method it runs may be any.
115
+ def unresolved_call?(node, scope)
116
+ return false unless node.is_a?(Prism::CallNode)
117
+
118
+ receiver = node.receiver
119
+ return false if receiver.nil? || receiver.is_a?(Prism::SelfNode)
120
+
121
+ targets = ProjectMethodOwnership.targets(GuardRebinding::ScanScope.dispatch_type(receiver, scope))
122
+ targets.nil? || targets.empty?
123
+ rescue StandardError
124
+ true
125
+ end
126
+
127
+ # `{ method name => [[class name, kind], …] }` over the project's `def` tables, kept for the tables it was
128
+ # built from.
129
+ def def_index(scope)
130
+ instance = scope.discovered_def_nodes
131
+ singleton = scope.discovered_singleton_def_nodes
132
+ cached = @def_index
133
+ return cached[2] if cached && cached[0].equal?(instance) && cached[1].equal?(singleton)
134
+
135
+ index = {}
136
+ { instance: instance, singleton: singleton }.each do |kind, table|
137
+ table.each do |class_name, methods|
138
+ methods.each_key { |method_name| (index[method_name] ||= []) << [class_name, kind] }
139
+ end
140
+ end
141
+ @def_index = [instance, singleton, index.freeze]
142
+ index
143
+ end
144
+
145
+ # A breadth-first search by method name from a node to a writer's name.
146
+ class Search
147
+ # The most `def` bodies one search reads before it answers that it reaches.
148
+ DEF_BUDGET = 64
149
+ SEND_CALLS = %i[send __send__ public_send method public_method instance_method].to_set.freeze
150
+ CODE_CALLS = %i[eval instance_eval instance_exec class_eval class_exec module_eval module_exec].to_set.freeze
151
+
152
+ class Reached < StandardError
153
+ end
154
+
155
+ def initialize(scope, writers)
156
+ @scope = scope
157
+ @writers = writers
158
+ @aliases = GlobalWriterReach.alias_table(scope)
159
+ @recording = Analysis::DependencyRecorder.active?
160
+ @seen = Set.new
161
+ @queue = []
162
+ @read = 0
163
+ end
164
+
165
+ def reaches?(node)
166
+ scan(node)
167
+ until @queue.empty?
168
+ name = @queue.shift
169
+ definitions = definitions_of(name)
170
+ enqueue(:method_missing) if definitions.empty?
171
+ definitions.each { |definition| read_def(definition) }
172
+ end
173
+ false
174
+ rescue Reached
175
+ true
176
+ end
177
+
178
+ private
179
+
180
+ def read_def(definition)
181
+ @read += 1
182
+ reached! if @read > DEF_BUDGET
183
+ scan(definition.parameters)
184
+ scan(definition.body)
185
+ end
186
+
187
+ def definitions_of(name)
188
+ Analysis::DependencyRecorder.read_name(:defines, name) if Analysis::DependencyRecorder.active?
189
+ targets = GlobalWriterReach.def_index(@scope)[name] || []
190
+ targets.filter_map do |class_name, kind|
191
+ kind == :singleton ? @scope.singleton_def_for(class_name, name) : @scope.user_def_for(class_name, name)
192
+ end
193
+ end
194
+
195
+ def scan(node)
196
+ return unless node.is_a?(Prism::Node)
197
+ # A nested `def` defines a method; it runs nothing here, and its name is followed where it is called.
198
+ return if node.is_a?(Prism::DefNode)
199
+
200
+ case node
201
+ when Prism::CallNode then scan_call(node)
202
+ when Prism::BlockArgumentNode then scan_block_argument(node)
203
+ when Prism::ForNode then enqueue(:each)
204
+ end
205
+ implicit_names(node).each { |name| enqueue(name) }
206
+ node.rigor_each_child { |child| scan(child) }
207
+ end
208
+
209
+ def scan_call(node)
210
+ enqueue(node.name)
211
+ # `new` runs the class's `initialize`.
212
+ enqueue(:initialize) if node.name == :new
213
+ arguments = node.arguments&.arguments || []
214
+ if SEND_CALLS.include?(node.name)
215
+ name = literal_name(arguments.first)
216
+ name ? enqueue(name) : reached!
217
+ elsif CODE_CALLS.include?(node.name) && !(arguments.empty? && node.block.is_a?(Prism::BlockNode))
218
+ reached!
219
+ end
220
+ end
221
+
222
+ def scan_block_argument(node)
223
+ name = literal_name(node.expression)
224
+ enqueue(name) if name
225
+ end
226
+
227
+ # The methods an operator or compound write calls without spelling them.
228
+ def implicit_names(node)
229
+ names = []
230
+ names << node.binary_operator if node.respond_to?(:binary_operator)
231
+ names.push(node.read_name, node.write_name) if node.respond_to?(:read_name) && node.respond_to?(:write_name)
232
+ names
233
+ end
234
+
235
+ def enqueue(name)
236
+ return if name.nil? || @seen.include?(name)
237
+
238
+ reached! if @writers.include?(name)
239
+ @seen << name
240
+ @queue << name
241
+ # An alias runs the method it names.
242
+ Analysis::DependencyRecorder.read_name(:global_census, "method_alias:#{name}") if @recording
243
+ @aliases[name]&.each { |original| enqueue(original) }
244
+ end
245
+
246
+ def literal_name(node)
247
+ return unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
248
+
249
+ text = node.unescaped
250
+ text.to_sym if text.valid_encoding?
251
+ end
252
+
253
+ def reached!
254
+ raise Reached
255
+ end
256
+ end
257
+ private_constant :Search
258
+ end
259
+ end
260
+ end
@@ -41,8 +41,7 @@ module Rigor
41
41
 
42
42
  # Reference to a formal parameter the enclosing `Definition#params` declared. The reducer
43
43
  # substitutes this node with the matching positional arg from the `App` being reduced; an
44
- # unknown name raises during reduction (the parser, when it ships, MUST reject unknown names
45
- # earlier).
44
+ # unknown name raises during reduction.
46
45
  Param = Data.define(:name) do
47
46
  def initialize(name:)
48
47
  raise ArgumentError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
@@ -43,7 +43,7 @@ module Rigor
43
43
  # `Rigor::Type::*` carrier: `nil` / `true` / `false` produce `Constant` carriers; `bool`
44
44
  # produces the `Constant<true> | Constant<false>` union; `untyped` produces
45
45
  # `Combinator.untyped` (i.e. `Dynamic[Top]`). Nominal class names produce raw `Type::Nominal`
46
- # carriers (no `name_scope` resolution at this slice — the reducer trusts the name verbatim).
46
+ # carriers (no `name_scope` resolution; the reducer trusts the name verbatim).
47
47
  module HktBodyParser
48
48
  class ParseError < StandardError; end
49
49
 
@@ -151,10 +151,8 @@ module Rigor
151
151
  # test := type_expr ("<:" | "==") type_expr
152
152
  #
153
153
  # Parens delimit a conditional unambiguously — bare `(type_expr)` grouping is not
154
- # supported at this slice (no use case yet). Branches can be unions; test sides are
155
- # single arms (wrap in `App[my_union, ...]` if you need a union there). `in [opt1, opt2]`
156
- # membership tests are programmatically supported via `HktBody::TestMembership` but the
157
- # parser does not yet recognise the `in` keyword (no concrete demand yet).
154
+ # supported (no use case yet). Branches can be unions; test sides are single arms (wrap
155
+ # in `App[my_union, ...]` if you need a union there).
158
156
  def parse_conditional
159
157
  expect!(:lparen)
160
158
  test = parse_test
@@ -152,8 +152,8 @@ module Rigor
152
152
  def subtype_verdict(left, right)
153
153
  return :yes if left == right
154
154
 
155
- # Both Nominals with different class names AND neither carries Dynamic — `:no`
156
- # (statically disjoint). For any other shape pair, `:maybe`.
155
+ # Two Nominals of different class names are statically disjoint (`:no`); any other shape
156
+ # pair is `:maybe`.
157
157
  return :no if disjoint_nominals?(left, right)
158
158
  return :no if disjoint_constants?(left, right)
159
159
 
@@ -9,11 +9,9 @@ module Rigor
9
9
  # annotations in shipped `.rbs` files.
10
10
  #
11
11
  # The registry stores registration metadata (arity, variance, bound) and the definition body
12
- # as both a raw String and an evaluable `HktBody` node tree. `HktReducer` (Slice 2a) and
13
- # `HktBodyParser` (Slice 2b) are both shipped and reduce `Type::App` instances against the
14
- # definition. The carrier never needs to read from the registry because Slice 1's `Type::App`
15
- # carries its `bound` directly; the registry exists at this slice solely so the parser
16
- # round-trip and downstream slices have a stable target API.
12
+ # as both a raw String and an evaluable `HktBody` node tree. `HktReducer` and `HktBodyParser`
13
+ # reduce `Type::App` instances against the definition. The carrier never needs to read from the
14
+ # registry because `Type::App` carries its `bound` directly.
17
15
  #
18
16
  # The registry is immutable after construction. Callers that need to extend it (e.g. plugin
19
17
  # registrations layered on top of stdlib registrations) MUST build a new registry via `merge`
@@ -9,8 +9,7 @@ require_relative "unknown_store_widening"
9
9
  module Rigor
10
10
  module Inference
11
11
  # Closes the "`params[:f] ||= []; params[:f] << x`" precision gap surfaced by the Redmine
12
- # 6.1.2 `Query#as_params` survey (ROADMAP § Future cycles / Type-language / engine —
13
- # "Indexed-collection narrowing through `Hash[k] ||= default`").
12
+ # 6.1.2 `Query#as_params` survey.
14
13
  #
15
14
  # After `receiver[key] ||= default` the next read at `receiver[key]` is known non-nil, but
16
15
  # Rigor types each `Hash#[]` independently and the subsequent `<<` / `[]=` / other mutator
@@ -206,11 +205,9 @@ module Rigor
206
205
  args&.size == 1 ? stable_address(node.receiver, args.first) : nil
207
206
  end
208
207
 
209
- # Companion invalidator for single-hop method-chain narrowings (ROADMAP § Future cycles —
210
- # "Method-call receiver narrowing across stable receivers", B2 from the slice's design
211
- # notes). Drops every `(receiver, *)` chain narrowing rooted at the call's OUTER stable
212
- # receiver — matching the ROADMAP's "any intervening method call against the same
213
- # receiver" criterion. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
208
+ # Companion invalidator for single-hop method-chain narrowings. Drops every `(receiver, *)`
209
+ # chain narrowing rooted at the call's OUTER stable receiver, for any intervening method call
210
+ # against the same receiver. A call against `x.last` (the OUTER receiver is a `CallNode`, not a
214
211
  # stable root) does NOT drop narrowings keyed on `x`, so the worked-site `x.last << y`
215
212
  # pattern correctly preserves the chain narrowing for any further `x.last` read in the same
216
213
  # body. Always-safe (only forgets; never invents).
@@ -19,8 +19,8 @@ module Rigor
19
19
  # the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
20
20
  # them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
21
21
  module InterveningIvarReset
22
- # Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class or
23
- # module body, where `@x` is the class's own instance variable.
22
+ # Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class,
23
+ # module or `class << …` body, where `@x` is the class's own instance variable.
24
24
  NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
25
25
  # Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
26
26
  # method's block, and `super` the superclass's method on `self`.
@@ -180,12 +180,17 @@ module Rigor
180
180
  # takes the mark when widened, so a `nil` only the seed brings back is not fuel for
181
181
  # `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
182
182
  # (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
183
- def widen(scope, mark: false)
183
+ # Issue #2039 — `mark: :unproven` marks as `mark: true` does only when the block answers nil (no write set is
184
+ # proven) and `direct_writes` (called at most once, answering a `Set`) does not name the variable: a call
185
+ # proven to write it, or whose callee visibly assigns it a possibly-nil value ({DirectIvarWrites}), widens it
186
+ # unmarked.
187
+ def widen(scope, mark: false, direct_writes: nil)
184
188
  seed = class_ivar_seed(scope)
185
189
  return scope if seed.nil? || seed.empty?
186
190
 
187
191
  computed = false
188
192
  writes = nil
193
+ direct = lazy_set(direct_writes)
189
194
  seed.reduce(scope) do |widened, (name, seed_type)|
190
195
  local_type = scope.ivar(name)
191
196
  next widened if local_type.nil? || local_type == seed_type
@@ -198,11 +203,28 @@ module Rigor
198
203
 
199
204
  joined = Type::Combinator.union(local_type, seed_type)
200
205
  widened = widened.with_ivar(name, joined)
201
- marked = (mark && nil_free?(local_type)) || scope.declaration_sourced?(:ivar, name)
206
+ marked = widen_marks?(mark, writes, direct, name, local_type) || scope.declaration_sourced?(:ivar, name)
202
207
  marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
203
208
  end
204
209
  end
205
210
 
211
+ # Whether {.widen} stamps the declaration mark on `name`'s binding, narrowed to `local_type`: one whose
212
+ # narrowing ruled `nil` out, under `mark: true`, or under `mark: :unproven` when no write set is proven
213
+ # (`writes` nil) and the callee does not visibly write it (`direct`).
214
+ def widen_marks?(mark, writes, direct, name, local_type)
215
+ return false unless mark && nil_free?(local_type)
216
+
217
+ mark != :unproven || (writes.nil? && !direct.call.include?(name))
218
+ end
219
+ private_class_method :widen_marks?
220
+
221
+ # A callable answering `callable`'s `Set`, computed on the first call; an empty one when `callable` is nil.
222
+ def lazy_set(callable)
223
+ value = nil
224
+ -> { value ||= callable ? callable.call : Set.new }
225
+ end
226
+ private_class_method :lazy_set
227
+
206
228
  # Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
207
229
  # candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
208
230
  # so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
@@ -22,6 +22,11 @@ module Rigor
22
22
  # - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
23
23
  # {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
24
24
  # file writes the same ivar of the same class.
25
+ # - `[:own_write_type, class_name, name, value]` — issues #2079 / #2170: the same write with its coarse value, a
26
+ # literal's as `[:lit, value]` and anything else as {DYNAMIC}, and a class-body `attr_writer` /
27
+ # `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. The class-ivar census
28
+ # of a file joins another file's entries, and those of the modules and superclasses on the class's chain,
29
+ # into the ivars it already holds ({ScopeIndexer::IvarContributions}).
25
30
  class IvarWriterCensus
26
31
  SEND_CALLS = %i[send __send__ public_send].to_set.freeze
27
32
  # The methods a `send` reaching which is an open-world writer.
@@ -34,6 +39,10 @@ module Rigor
34
39
  attr_writer attr_accessor send public_send __send__
35
40
  ].to_set.freeze
36
41
  NO_ARGUMENTS = [].freeze
42
+ # Issue #2079 — the coarse value of a write that is not a literal.
43
+ DYNAMIC = :dynamic
44
+ # The writer declarations a class body records as a {DYNAMIC} write of each name it spells.
45
+ WRITER_DECLARATIONS = %i[attr_writer attr_accessor].to_set.freeze
37
46
 
38
47
  # The call names any rule here reads, so every other call returns at one Set probe.
39
48
  # (`define_method` is in {SINGLETON_CLASS_CALLS}.)
@@ -44,6 +53,7 @@ module Rigor
44
53
  @instance_ranges = []
45
54
  @owner_names = {}.compare_by_identity
46
55
  @own_writes = {}
56
+ @typed_writes = Set.new
47
57
  end
48
58
 
49
59
  # Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
@@ -67,6 +77,7 @@ module Rigor
67
77
  owner = instance_owner(node)
68
78
  return if owner.nil?
69
79
 
80
+ record_typed_write(owner, node.name, coarse_value(node))
70
81
  seen = (@own_writes[owner] ||= {})
71
82
  return if seen[node.name]
72
83
 
@@ -74,9 +85,49 @@ module Rigor
74
85
  @census << [:own_write, owner_name(owner), node.name].freeze
75
86
  end
76
87
 
88
+ def record_typed_write(owner, name, value)
89
+ entry = [:own_write_type, owner_name(owner), name, value].freeze
90
+ @census << entry if @typed_writes.add?(entry)
91
+ end
92
+
93
+ # Issue #2079 — a literal's value as `[:lit, value]`; any other value, an operator write and a multiple
94
+ # assignment target as {DYNAMIC}.
95
+ def coarse_value(node)
96
+ value = node.value if node.respond_to?(:value) && !node.is_a?(Prism::InstanceVariableOperatorWriteNode)
97
+ case value
98
+ when Prism::NilNode then [:lit, nil].freeze
99
+ when Prism::TrueNode then [:lit, true].freeze
100
+ when Prism::FalseNode then [:lit, false].freeze
101
+ when Prism::IntegerNode, Prism::FloatNode then [:lit, value.value].freeze
102
+ when Prism::SymbolNode, Prism::StringNode then literal_text(value)
103
+ else DYNAMIC
104
+ end
105
+ end
106
+
107
+ def literal_text(node)
108
+ text = node.unescaped
109
+ return DYNAMIC unless text.valid_encoding?
110
+
111
+ [:lit, node.is_a?(Prism::SymbolNode) ? text.to_sym : text.dup.freeze].freeze
112
+ end
113
+
114
+ # Issue #2079 — `attr_writer :x` / `attr_accessor :x` in a class body writes `@x` with whatever the caller hands.
115
+ def record_writer_declaration(node, owner)
116
+ return unless owner && WRITER_DECLARATIONS.include?(node.name) && self_receiver?(node)
117
+ # A guard: an instance body's own `attr_writer` call raises (the method is `Module`'s), so only the class
118
+ # body's declares a writer.
119
+ return unless instance_owner(node).nil?
120
+
121
+ (node.arguments&.arguments || NO_ARGUMENTS).each do |argument|
122
+ name = literal_name(argument)
123
+ record_typed_write(owner, :"@#{name}", DYNAMIC) if name
124
+ end
125
+ end
126
+
77
127
  def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
78
128
 
79
129
  def visit_call(node, owner)
130
+ record_writer_declaration(node, owner)
80
131
  remember(node.block.location, owner) if owner && define_method_block?(node)
81
132
  if self_receiver?(node) || own_singleton_class_call?(node)
82
133
  return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../type"
6
6
  require_relative "mutation_widening"
7
7
  require_relative "key_presence_guard/scan"
8
+ require_relative "../run_scoped_slots"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -58,7 +59,7 @@ module Rigor
58
59
  ).to_set.freeze
59
60
 
60
61
  STATE_KEY = :__rigor_key_presence_guard
61
- CACHE_KEY = :__rigor_key_presence_guard_rewalk
62
+ CACHE_KEY = RunScopedSlots.register(:__rigor_key_presence_guard_rewalk)
62
63
  private_constant :STATE_KEY, :CACHE_KEY
63
64
 
64
65
  # The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../run_scoped_slots"
4
+
3
5
  module Rigor
4
6
  module Inference
5
7
  # The value the statement evaluator last gave a loop node, and the scope it evaluated the loop from, so
@@ -13,15 +15,17 @@ module Rigor
13
15
  # evaluation would. It is not compared with `Scope#==`, which ignores several fields (`match_frame`,
14
16
  # `declared_refinements`, `struct_fold_safe_locals`, the opaque-block flags, `lexical_nesting`, the origins tables,
15
17
  # `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
16
- # all (issue #2262). It lives in a per-thread map whose keys are held weakly, so a record goes when its parse tree
17
- # does.
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.
18
22
  module LoopValueRecord
19
- KEY = :rigor_loop_value_record
23
+ KEY = RunScopedSlots.register(:rigor_loop_value_record)
20
24
  private_constant :KEY
21
25
 
22
26
  module_function
23
27
 
24
- COUNT_KEY = :rigor_loop_value_count
28
+ COUNT_KEY = RunScopedSlots.register(:rigor_loop_value_count)
25
29
  private_constant :COUNT_KEY
26
30
 
27
31
  def record(node, scope, type)
@@ -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,17 @@ 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
+
91
110
  # Issue #1157 — whether a call's `&expr` block argument hands on the block THIS method was called with: the
92
111
  # anonymous `&` of a method that declares an anonymous `&` parameter, or {#forwarded_block?} for `&name`.
93
112
  # Anything else (`&:sym`, a proc made here, a rebound name) is a block of this frame's own making.
@@ -144,6 +163,33 @@ module Rigor
144
163
  !block.nil? && block.name.nil?
145
164
  end
146
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
+
147
193
  # The method's own `&block` parameter's name, or nil.
148
194
  def block_parameter_name
149
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