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,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
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
8
+ # ({ScopeIndexer::DeclaredParamWrites}), which must not type an ivar a writer the class-ivar census never walks may
9
+ # replace. The standard is parity with a literal write: `@x = 1` in `initialize` already reads `1` in every sibling
10
+ # method whatever an unknown object's code does, so only the writers that literal writes would also have to answer
11
+ # for are recorded, and an unknown-receiver writer records nothing (as `ReflectiveIvarWrite` leaves
12
+ # `o.instance_variable_set` alone). Three entry kinds go into the census `Set`:
13
+ #
14
+ # - `[:ivar_set, name]` — a literal `o.instance_variable_set(:@x, v)` (or the same through a `send` that spells
15
+ # it) on an explicit receiver other than `self`, and an `@x` write in the literal block of an
16
+ # `o.instance_eval { … }` / `instance_exec`: code that names the ivar it writes.
17
+ # - `[:ivar_set_class, class_name]` — an open-world writer ({#open_world?}) with an implicit or `self` receiver
18
+ # inside an instance body of the class — a `def` or a `define_method` block in its class body, in the block of a
19
+ # `K = Struct.new` / `Data.define` / `Class.new` constant, or in a `K.class_eval` block (the host walk passes
20
+ # that class as `owner`): it may write any ivar of the class's instances. A writer in a class body, a
21
+ # `def self.` or at the top level records nothing.
22
+ # - `[:own_write, class_name, name]` — an `@x` write inside an instance body of the class.
23
+ # {ScopeIndexer#merge_constant_literal_tables} appends the file's path, so a reader can tell whether a second
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}).
30
+ class IvarWriterCensus
31
+ SEND_CALLS = %i[send __send__ public_send].to_set.freeze
32
+ # The methods a `send` reaching which is an open-world writer.
33
+ OPEN_WORLD_SENDS = %i[instance_variable_set instance_eval instance_exec extend
34
+ define_singleton_method].to_set.freeze
35
+ EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
36
+ # The calls on a `singleton_class` receiver that give the object methods or code the census cannot read.
37
+ SINGLETON_CLASS_CALLS = %i[
38
+ class_eval class_exec module_eval module_exec define_method include prepend extend attr attr_reader
39
+ attr_writer attr_accessor send public_send __send__
40
+ ].to_set.freeze
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
46
+
47
+ # The call names any rule here reads, so every other call returns at one Set probe.
48
+ # (`define_method` is in {SINGLETON_CLASS_CALLS}.)
49
+ WATCHED_CALLS = (SEND_CALLS | OPEN_WORLD_SENDS | EVAL_CALLS | SINGLETON_CLASS_CALLS).freeze
50
+
51
+ def initialize(census)
52
+ @census = census
53
+ @instance_ranges = []
54
+ @owner_names = {}.compare_by_identity
55
+ @own_writes = {}
56
+ @typed_writes = Set.new
57
+ end
58
+
59
+ # Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
60
+ # or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
61
+ # instance (`class << self`, the top level, an opaque block).
62
+ def visit(node, owner)
63
+ case node
64
+ when Prism::DefNode then remember(node.location, owner) if owner && node.receiver.nil?
65
+ when Prism::SingletonClassNode then visit_singleton_class(node)
66
+ when Prism::CallNode then visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
67
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
68
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
69
+ record_own_write(node) unless @instance_ranges.empty?
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ # One entry per class and ivar: a repeated write costs a Hash probe, not a location and an Array.
76
+ def record_own_write(node)
77
+ owner = instance_owner(node)
78
+ return if owner.nil?
79
+
80
+ record_typed_write(owner, node.name, coarse_value(node))
81
+ seen = (@own_writes[owner] ||= {})
82
+ return if seen[node.name]
83
+
84
+ seen[node.name] = true
85
+ @census << [:own_write, owner_name(owner), node.name].freeze
86
+ end
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
+
127
+ def owner_name(owner) = owner.is_a?(Array) ? (@owner_names[owner] ||= owner.join("::")) : owner
128
+
129
+ def visit_call(node, owner)
130
+ record_writer_declaration(node, owner)
131
+ remember(node.block.location, owner) if owner && define_method_block?(node)
132
+ if self_receiver?(node) || own_singleton_class_call?(node)
133
+ return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
134
+
135
+ instance = instance_owner(node)
136
+ record_class(instance) if instance
137
+ else
138
+ visit_foreign(node)
139
+ end
140
+ end
141
+
142
+ # `class << self` inside an instance `def` opens the instance's singleton class.
143
+ def visit_singleton_class(node)
144
+ return unless node.expression.is_a?(Prism::SelfNode) && !@instance_ranges.empty?
145
+
146
+ instance = instance_owner(node)
147
+ record_class(instance) if instance
148
+ end
149
+
150
+ def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
151
+
152
+ # The self writers whose target or code the census cannot read: a `send` with a computed method name or one
153
+ # naming {OPEN_WORLD_SENDS}; `instance_eval` / `instance_exec` given a block argument, and `instance_eval`
154
+ # given a string; `extend`; `define_singleton_method` with a body; and an `instance_variable_set` whose name no
155
+ # literal spells. A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` counts as well.
156
+ def open_world?(node)
157
+ name = node.name
158
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
159
+ return open_world_send?(arguments) if SEND_CALLS.include?(name)
160
+ return open_world_eval?(node, arguments) if EVAL_CALLS.include?(name)
161
+ # #1931's census already makes every ivar of the class gradual for one in the same file; this entry covers one
162
+ # in a reopening in another file, which that census does not see.
163
+ return ivar_name(arguments.first).nil? if name == :instance_variable_set
164
+
165
+ name == :extend || (name == :define_singleton_method && (!node.block.nil? || arguments.size > 1))
166
+ end
167
+
168
+ def open_world_send?(arguments)
169
+ return false if arguments.empty?
170
+
171
+ sent = literal_name(arguments.first)
172
+ sent.nil? || OPEN_WORLD_SENDS.include?(sent)
173
+ end
174
+
175
+ def open_world_eval?(node, arguments)
176
+ node.block.is_a?(Prism::BlockArgumentNode) || (node.name == :instance_eval && !arguments.empty?)
177
+ end
178
+
179
+ # A call on another receiver: a named foreign write, or `self.singleton_class.class_eval` and its kin.
180
+ def visit_foreign(node)
181
+ arguments = node.arguments&.arguments || NO_ARGUMENTS
182
+ name = node.name
183
+ if SEND_CALLS.include?(name) && arguments.first && literal_name(arguments.first) == :instance_variable_set
184
+ name = :instance_variable_set
185
+ arguments = arguments.drop(1)
186
+ end
187
+ if name == :instance_variable_set
188
+ ivar = ivar_name(arguments.first)
189
+ @census << [:ivar_set, ivar].freeze if ivar
190
+ elsif EVAL_CALLS.include?(name) && node.block.is_a?(Prism::BlockNode)
191
+ record_block_ivar_writes(node.block)
192
+ end
193
+ end
194
+
195
+ def record_block_ivar_writes(node)
196
+ return unless node.is_a?(Prism::Node)
197
+ return if node.is_a?(Prism::DefNode) || node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
198
+
199
+ case node
200
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
201
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
202
+ @census << [:ivar_set, node.name].freeze
203
+ end
204
+ node.rigor_each_child { |child| record_block_ivar_writes(child) }
205
+ end
206
+
207
+ def define_method_block?(node)
208
+ node.name == :define_method && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
209
+ end
210
+
211
+ # An instance body (`def` or `define_method` block) and the class it belongs to. A `define_method` block's own
212
+ # nodes may reach the walk with no owner (the walk reads its `self` as opaque), so the class rides the range.
213
+ def remember(location, owner)
214
+ @instance_ranges << [(location.start_offset...location.end_offset), owner]
215
+ end
216
+
217
+ # The class of the innermost instance body covering `node` (pre-order, so the last covering range), or nil.
218
+ def instance_owner(node)
219
+ offset = node.location.start_offset
220
+ @instance_ranges.reverse_each { |(range, owner)| return owner if range.cover?(offset) }
221
+ nil
222
+ end
223
+
224
+ def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
225
+
226
+ # A call in {SINGLETON_CLASS_CALLS} on the object's own `singleton_class` (`singleton_class.class_eval { … }`).
227
+ def own_singleton_class_call?(node)
228
+ receiver = node.receiver
229
+ receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && self_receiver?(receiver) &&
230
+ SINGLETON_CLASS_CALLS.include?(node.name)
231
+ end
232
+
233
+ def literal_name(argument)
234
+ return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
235
+
236
+ text = argument.unescaped
237
+ text.to_sym if text.valid_encoding?
238
+ end
239
+
240
+ def ivar_name(argument)
241
+ name = literal_name(argument)
242
+ name if name.to_s.start_with?("@")
243
+ end
244
+ end
245
+ end
246
+ end
@@ -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.