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
@@ -2,21 +2,21 @@
2
2
 
3
3
  module Rigor
4
4
  class Environment
5
- # Frozen, `Ractor.shareable?` read-only RBS query facade.
5
+ # Frozen read-only RBS query facade.
6
6
  # [ADR-15](../../../docs/adr/15-ractor-concurrency.md) Phase 2b extracts the read-only surface of
7
7
  # {RbsLoader} into this carrier so future Ractor-isolated workers can share one Reflection across the
8
8
  # pool while keeping the per-Ractor mutable accelerator state (per-process memo Hashes) where it belongs.
9
9
  #
10
10
  # Backing tables (all frozen at construction):
11
11
  #
12
- # - `known_class_names` — `Set<String>` of every class / module / alias name in the loaded RBS
13
- # environment. Top-level prefixed (`"::Hash"`); plain queries normalise via {#normalise}.
14
- # - `instance_definitions` — `Hash<String, RBS::Definition>` keyed on `RBS::TypeName#to_s` (top-level
12
+ # - `known_class_names` — every class / module / alias name in the loaded RBS environment.
13
+ # Top-level prefixed (`"::Hash"`); the query methods accept either form.
14
+ # - `instance_definitions` — `RBS::Definition` per class, keyed on `RBS::TypeName#to_s` (top-level
15
15
  # prefixed).
16
16
  # - `singleton_definitions` — same shape, singleton side.
17
- # - `type_param_names` — `Hash<String, Array<Symbol>>` of declared type parameters per class.
18
- # - `constant_types` — `Hash<String, Rigor::Type>` of translated constant declarations.
19
- # - `ancestor_names` — `Hash<String, Array<String>>` of normalised ancestor chains per class.
17
+ # - `type_param_names` — declared type parameters per class.
18
+ # - `constant_types` — translated constant declarations.
19
+ # - `ancestor_names` — normalised ancestor chains per class.
20
20
  #
21
21
  # Each `Reflection` instance is `frozen?` at construction — every cached table is frozen, `self` is
22
22
  # frozen. **NOT** `Ractor.shareable?`: the `instance_definitions` / `singleton_definitions` tables hold
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../analysis/path_expansion"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class Environment
@@ -42,6 +43,36 @@ module Rigor
42
43
  # anything.
43
44
  PATTERN = /(?:(?<![\w.$@])|(?<=Kernel\.))require[ \t]*\(?[ \t]*(["'])(#{FEATURE_ALTERNATION})\1/
44
45
 
46
+ # Issue #2174 — the gems Ruby 4.0 bundles (its `gems/bundled_gems` list, which an installation does not
47
+ # keep): installed beside the interpreter, but loaded only by an explicit `require`. Gems installed later
48
+ # into the same directory are deliberately not read from it.
49
+ BUNDLED_GEMS = %w[
50
+ abbrev base64 benchmark bigdecimal csv debug drb fiddle getoptlong irb logger matrix minitest mutex_m
51
+ net-ftp net-imap net-pop net-smtp nkf observer ostruct power_assert prime pstore racc rake rbs rdoc readline
52
+ reline repl_type_completor resolv-replace rexml rinda rss syslog test-unit typeprof win32ole
53
+ ].freeze
54
+
55
+ # Issue #2174 — the first path segments of the features Ruby ships: the `.rb` files and directories of the
56
+ # interpreter's library directory, the extensions and directories of its architecture directory, and the
57
+ # bundled gems' names (`net-ftp` → `net`). Read once, at load time and on the main Ractor, since
58
+ # `RbConfig::CONFIG` is not shareable; empty when the directories cannot be read.
59
+ SHIPPED_SEGMENTS = begin
60
+ require "rbconfig"
61
+ segments = Set.new
62
+ [RbConfig::CONFIG["rubylibdir"], RbConfig::CONFIG["archdir"]].compact.each do |dir|
63
+ next unless File.directory?(dir)
64
+
65
+ Dir.children(dir).each { |entry| segments << entry.sub(/\.(?:rb|so|bundle|dll)\z/, "") }
66
+ end
67
+ BUNDLED_GEMS.each { |name| segments << name.split("-").first }
68
+ segments.to_set(&:freeze).freeze
69
+ rescue StandardError
70
+ Set.new.freeze
71
+ end
72
+
73
+ # A `require` of any literal feature, read as {PATTERN} reads one; {.features_of} keeps those Ruby ships.
74
+ ANY_FEATURE = %r{(?:(?<![\w.$@])|(?<=Kernel\.))require[ \t]*\(?[ \t]*(["'])([\w-]+(?:/[\w-]+)*)\1}
75
+
45
76
  module_function
46
77
 
47
78
  # The features a run over `configuration` requires: every file under the configured paths, plus
@@ -53,37 +84,51 @@ module Rigor
53
84
  def for_configuration(configuration, extra_files = [], sources: {})
54
85
  files = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns,
55
86
  base: configuration.exclude_base)
56
- scan(files + Array(extra_files).map(&:to_s), sources: sources)
87
+ scan(files + Array(extra_files).map(&:to_s), sources: sources,
88
+ sig_paths: configured_sig_paths(configuration))
57
89
  rescue StandardError
58
90
  []
59
91
  end
60
92
 
93
+ # The project signature roots the environment loads: the configured `signature_paths:`, else `./sig` when it
94
+ # exists (`Environment.project_signature_roots`, which this leaf file does not load).
95
+ def configured_sig_paths(configuration)
96
+ configured = configuration.signature_paths
97
+ return Array(configured).map(&:to_s) if configured
98
+
99
+ File.directory?("sig") ? ["sig"] : []
100
+ end
101
+
61
102
  # @param files — source paths (Strings or Pathnames); an unreadable one contributes nothing.
62
103
  # @param sources — path => source text, scanned instead of the file; its paths are scanned even when
63
104
  # `files` does not list them.
64
- # @return the sorted, frozen features some file requires.
65
- def scan(files, sources: {})
105
+ # @param sig_paths — the project's signature roots, whose `.rbs` declaration headers count as the project's
106
+ # own names ({.relevant}).
107
+ # @return the sorted, frozen features some file requires, a shipped one kept only when {.relevant}.
108
+ def scan(files, sources: {}, sig_paths: [])
66
109
  sources = sources.to_h { |path, text| [File.expand_path(path.to_s), text] }
67
110
  paths = (Array(files).map { |path| source_key(File.expand_path(path.to_s), sources) } + sources.keys).uniq
68
111
  found = Set.new
112
+ names = Set.new
69
113
  paths.each do |path|
70
- break if found.size == VENDORED_DIRS.size
71
-
72
- found.merge(sources.key?(path) ? features_of(sources[path]) : memoized_features_in(path))
114
+ features, declared = sources.key?(path) ? scan_source(sources[path]) : memoized_scan_in(path)
115
+ found.merge(features)
116
+ names.merge(declared)
73
117
  end
74
118
  prune_memo(paths)
75
- found.to_a.sort.freeze
119
+ relevant(found, names.merge(sig_names(sig_paths))).sort.freeze
76
120
  end
77
121
 
78
- # @return path (absolute) => the features that file requires, for every file under the configured paths
79
- # that requires any. A long-lived reader (the language server) keeps this and overlays its open buffers.
122
+ # @return path (absolute) => `[features, declared names]` ({.scan_source}) for every file under the
123
+ # configured paths that has either. A long-lived reader (the language server) keeps this, overlays its open
124
+ # buffers and passes the union to {.relevant}.
80
125
  def feature_files(configuration)
81
126
  listed = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns,
82
127
  base: configuration.exclude_base)
83
128
  files = listed.map { |path| File.expand_path(path.to_s) }
84
129
  result = files.each_with_object({}) do |path, acc|
85
- features = memoized_features_in(path)
86
- acc[path] = features unless features.empty?
130
+ scanned = memoized_scan_in(path)
131
+ acc[path] = scanned unless scanned.all?(&:empty?)
87
132
  end
88
133
  prune_memo(files)
89
134
  result
@@ -91,6 +136,42 @@ module Rigor
91
136
  {}
92
137
  end
93
138
 
139
+ # Issue #2174 — `features` with each shipped feature kept only when one of `names` (constant names the
140
+ # project's source or signatures declare a class or module under) is a root the feature's library declares
141
+ # ({RubyLibraryRoots}). Only such a feature can open a class (`RbsLoader#ruby_library_reopen?` asks it of a
142
+ # class every declaration of which is the project's), so dropping the rest changes no diagnostic, and keeps a
143
+ # `require "json"` edit in a project that reopens no library class out of the incremental snapshot's
144
+ # fingerprint and the language server's environment key. The listed features ({VENDORED_DIRS}) are kept.
145
+ def relevant(features, names)
146
+ return features.to_a if features.all? { |feature| VENDORED_DIRS.key?(feature) }
147
+
148
+ segments = names.empty? ? EMPTY_SEGMENTS : library_segments(names)
149
+ features.select { |feature| VENDORED_DIRS.key?(feature) || segments.include?(feature.split("/").first) }
150
+ end
151
+
152
+ # The feature segments of the libraries that declare one of `names` as a root. Loaded lazily: the roots read
153
+ # the `rbs` stdlib tree, which a run with no shipped `require` never needs.
154
+ def library_segments(names)
155
+ require_relative "ruby_library_roots"
156
+ roots = RubyLibraryRoots.shared
157
+ names.each_with_object(Set.new) { |name, acc| acc.merge(roots.fetch(name, EMPTY_SEGMENTS)) }
158
+ end
159
+
160
+ EMPTY_SEGMENTS = [].freeze
161
+
162
+ # The constant names the `.rbs` files under `sig_paths` declare a class or module under.
163
+ def sig_names(sig_paths)
164
+ Array(sig_paths).each_with_object(Set.new) do |root, acc|
165
+ next unless File.directory?(root.to_s)
166
+
167
+ RootedGlob.glob(root.to_s, "**/*.rbs").each do |path|
168
+ acc.merge(declared_names(File.binread(path)))
169
+ rescue SystemCallError, IOError
170
+ next
171
+ end
172
+ end
173
+ end
174
+
94
175
  # Paths are compared expanded, so a relative `--instead-of=lib/a.rb` replaces the configured
95
176
  # `/project/lib/a.rb`; a source whose real path matches (a symlinked directory) replaces it too. The real
96
177
  # path is asked only for a file that shares a source's basename, so a scan does not resolve every path.
@@ -114,8 +195,8 @@ module Rigor
114
195
  # build; a long-lived language server on every rebuild), so on the main Ractor each file's answer is kept
115
196
  # against its stat and the file is re-read only when the stat moved. A non-main Ractor may not touch the
116
197
  # module's state and reads the file every time.
117
- def memoized_features_in(path)
118
- return features_in(path) unless Ractor.current == Ractor.main
198
+ def memoized_scan_in(path)
199
+ return scan_in(path) unless Ractor.current == Ractor.main
119
200
 
120
201
  stat = File.stat(path)
121
202
  stamp = [stat.mtime.to_r, stat.ctime.to_r, stat.size, stat.ino]
@@ -123,13 +204,15 @@ module Rigor
123
204
  cached = memo[path]
124
205
  return cached.last if cached && cached.first == stamp
125
206
 
126
- features = features_in(path)
127
- memo[path] = [stamp, features]
128
- features
207
+ scanned = scan_in(path)
208
+ memo[path] = [stamp, scanned]
209
+ scanned
129
210
  rescue SystemCallError
130
- []
211
+ NOTHING
131
212
  end
132
213
 
214
+ NOTHING = [[].freeze, [].freeze].freeze
215
+
133
216
  # Keeps the memo to the files the latest scan was asked about, so a long-lived language server holds one
134
217
  # entry per project file rather than one per file it ever saw.
135
218
  def prune_memo(paths)
@@ -139,28 +222,51 @@ module Rigor
139
222
  @memo.select! { |path, _| keep.include?(path) }
140
223
  end
141
224
 
142
- # @return the features one file requires.
143
- def features_in(path)
144
- features_of(File.binread(path))
225
+ # @return `[features, declared names]` for one file ({.scan_source}).
226
+ def scan_in(path)
227
+ scan_source(File.binread(path))
145
228
  rescue SystemCallError, IOError
146
- []
229
+ NOTHING
230
+ end
231
+
232
+ # @return `[features, declared names]`: what {.features_of} reads, and the constant names a `class` or
233
+ # `module` header in `source` spells, each segment on its own (`class Net::SMTP` gives `Net` and `SMTP`).
234
+ def scan_source(source)
235
+ [features_of(source), declared_names(source)].freeze
236
+ end
237
+
238
+ # A header on a line of its own, or inside a comment (an rbs-inline `# @rbs!` block declares
239
+ # `# class Matrix`). Over-matching a comment only keeps more features.
240
+ HEADER = /^[ \t]*(?:#[ \t]*)?(?:class|module)[ \t]+(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)/
241
+
242
+ def declared_names(source)
243
+ bytes = source.b
244
+ return [] unless bytes.include?("class") || bytes.include?("module")
245
+
246
+ bytes.scan(HEADER).flat_map { |(name)| name.split("::") }
247
+ .uniq.map { |name| name.dup.force_encoding(Encoding::UTF_8).freeze }.freeze
147
248
  end
148
249
 
149
- # @return the features `source` requires.
250
+ # @return the features `source` requires: the listed ones ({VENDORED_DIRS}), and, for issue #2174, every
251
+ # feature Ruby ships ({SHIPPED_SEGMENTS}), which `RbsLoader#ruby_library_reopen?` reads to tell a reopened
252
+ # library class from a project class of the same name. Only the listed ones become library tokens.
150
253
  def features_of(source)
151
- # A plain substring test first: almost no file mentions any listed feature, and it is far cheaper than
152
- # the regexp on a file that does not.
153
- return [] unless VENDORED_DIRS.each_key.any? { |feature| source.include?(feature) }
254
+ # A plain substring test first: a file with no `require` at all is the common case, and it is far cheaper
255
+ # than the regexp on a file that has none.
256
+ return [] unless source.include?("require")
154
257
 
155
258
  bytes = source.b
156
259
  named = []
157
260
  position = 0
158
- while (match = PATTERN.match(bytes, position))
261
+ while (match = ANY_FEATURE.match(bytes, position))
159
262
  named << match[2] unless commented?(bytes, match.begin(0))
160
263
  position = match.end(0)
161
264
  end
162
- # The table's own frozen keys, so the result is shareable across Ractors whatever the source's encoding.
163
- VENDORED_DIRS.each_key.select { |feature| named.include?(feature) }
265
+ # The table's own frozen keys, and frozen copies of the rest, so the result is shareable across Ractors
266
+ # whatever the source's encoding.
267
+ listed = VENDORED_DIRS.each_key.select { |feature| named.include?(feature) }
268
+ shipped = named.select { |feature| SHIPPED_SEGMENTS.include?(feature.split("/").first) }
269
+ (listed + shipped.map { |feature| feature.dup.force_encoding(Encoding::UTF_8).freeze }).uniq
164
270
  end
165
271
 
166
272
  # Whether the match at `offset` sits on a line whose first non-blank character is `#`: a whole-line comment.
@@ -176,7 +282,8 @@ module Rigor
176
282
  # The library tokens for `features` and for any listed feature the configuration's `libraries:` names.
177
283
  def tokens(features, configured_libraries)
178
284
  configured = Array(configured_libraries).map(&:to_s).select { |name| VENDORED_DIRS.key?(name) }
179
- (Array(features) | configured).sort.map { |feature| token(feature) }
285
+ listed = Array(features).select { |feature| VENDORED_DIRS.key?(feature) }
286
+ (listed | configured).sort.map { |feature| token(feature) }
180
287
  end
181
288
 
182
289
  def token(feature)
@@ -189,17 +296,14 @@ module Rigor
189
296
  name.start_with?(TOKEN_PREFIX) ? name.delete_prefix(TOKEN_PREFIX) : nil
190
297
  end
191
298
 
192
- # The vendored directory basenames a library list activates.
193
299
  def active_dirs(library_names)
194
300
  library_names.filter_map { |name| VENDORED_DIRS[feature_of_token(name)] }
195
301
  end
196
302
 
197
- # Whether `dir_basename` is one of the gated directories.
198
303
  def gated_dir?(dir_basename)
199
304
  VENDORED_DIRS.value?(dir_basename)
200
305
  end
201
306
 
202
- # The feature a gated directory loads for.
203
307
  def feature_for_dir(dir_basename)
204
308
  VENDORED_DIRS.key(dir_basename)
205
309
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+ require "rbconfig"
5
+ require_relative "missing_gem_constant_index"
6
+ require_relative "required_features"
7
+ require_relative "../rooted_glob"
8
+
9
+ module Rigor
10
+ class Environment
11
+ # Issue #2174 — which library Ruby ships declares each root constant name. Three sources are read, and no others:
12
+ #
13
+ # - every library of the `rbs` gem's `stdlib/` tree, loaded into this run or not, read from its `.rbs` files
14
+ # (`net-http` declares `Net`);
15
+ # - every top-level `.rb` file of the interpreter's library directory (`RbConfig::CONFIG["rubylibdir"]`), the
16
+ # default gems' and the standard library's entry files, read from their top-level declarations;
17
+ # - the entry file of each gem Ruby bundles ({RequiredFeatures::BUNDLED_GEMS}), found under the interpreter's
18
+ # gem directory. A gem installed there later is not one of them and is not read.
19
+ #
20
+ # Each root maps to the first path segments of the features that load it (`Matrix` → `matrix`, `Net` → `net`),
21
+ # which {RbsLoader#ruby_library_reopen?} intersects with the features the project's source requires
22
+ # ({RequiredFeatures.features_of}).
23
+ #
24
+ # Ownership is read, never guessed by camelizing a library name, for the reason {MissingGemConstantIndex}
25
+ # gives; that module's reader parses the `.rb` entry files. No library code runs. Everything fails open to
26
+ # "no library", which keeps the pre-#2174 reading. Core names (`Array`, `Kernel`) appear too, harmlessly:
27
+ # core signatures always load, so no project-only class carries one.
28
+ #
29
+ # The directories are resolved at load time, on the main Ractor: `RbConfig::CONFIG` and `Gem.default_dir`
30
+ # read non-shareable constants and raise `Ractor::IsolationError` inside a pool worker. The scan itself runs
31
+ # lazily (about 0.1 s), memoised per process, and only for a class about to be reported.
32
+ module RubyLibraryRoots
33
+ STDLIB_ROOT = begin
34
+ ::RBS::Repository::DEFAULT_STDLIB_ROOT.to_s.dup.freeze
35
+ rescue StandardError
36
+ nil
37
+ end
38
+
39
+ RUBY_LIB_DIR = begin
40
+ RbConfig::CONFIG["rubylibdir"].to_s.dup.freeze
41
+ rescue StandardError
42
+ nil
43
+ end
44
+
45
+ RUBY_GEMS_DIR = begin
46
+ File.join(Gem.default_dir, "gems").freeze
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ module_function
52
+
53
+ # {.build}, built once per process on the main Ractor, where the language server and the incremental
54
+ # snapshot fingerprint ask it repeatedly; a non-main Ractor may not keep module state and builds its own.
55
+ def shared
56
+ return build unless Ractor.current == Ractor.main
57
+
58
+ @shared ||= build
59
+ end
60
+
61
+ # The frozen map of root constant name to the frozen set of feature segments that load it.
62
+ def build
63
+ roots = Hash.new { |hash, key| hash[key] = Set.new }
64
+ stdlib_roots(roots)
65
+ ruby_lib_roots(roots)
66
+ bundled_gem_roots(roots)
67
+ roots.each_value(&:freeze)
68
+ roots.default_proc = nil
69
+ roots.freeze
70
+ end
71
+
72
+ def stdlib_roots(roots)
73
+ return if STDLIB_ROOT.nil? || !File.directory?(STDLIB_ROOT)
74
+
75
+ Dir.children(STDLIB_ROOT).each do |library|
76
+ segment = library.split("-").first
77
+ RootedGlob.glob(File.join(STDLIB_ROOT, library), "*/**/*.rbs").each do |path|
78
+ stdlib_file_roots(path).each { |root| roots[root] << segment }
79
+ end
80
+ end
81
+ end
82
+
83
+ def stdlib_file_roots(path)
84
+ _, _, decls = ::RBS::Parser.parse_signature(File.read(path))
85
+ decls.filter_map { |decl| root_of(decl) if named_declaration?(decl) }
86
+ rescue StandardError
87
+ []
88
+ end
89
+
90
+ def ruby_lib_roots(roots)
91
+ return if RUBY_LIB_DIR.nil? || !File.directory?(RUBY_LIB_DIR)
92
+
93
+ RootedGlob.glob(RUBY_LIB_DIR, "*.rb").each do |path|
94
+ segment = File.basename(path, ".rb")
95
+ MissingGemConstantIndex.top_level_root_constants(path).each { |root| roots[root] << segment }
96
+ end
97
+ end
98
+
99
+ def bundled_gem_roots(roots)
100
+ return if RUBY_GEMS_DIR.nil? || !File.directory?(RUBY_GEMS_DIR)
101
+
102
+ Dir.children(RUBY_GEMS_DIR).each do |basename|
103
+ gem_name = basename[/\A(.+?)-\d/, 1]
104
+ next if gem_name.nil? || !RequiredFeatures::BUNDLED_GEMS.include?(gem_name)
105
+
106
+ segment = gem_name.split("-").first
107
+ MissingGemConstantIndex.entry_files(File.join(RUBY_GEMS_DIR, basename), gem_name).each do |path|
108
+ MissingGemConstantIndex.top_level_root_constants(path).each { |root| roots[root] << segment }
109
+ end
110
+ end
111
+ end
112
+
113
+ def named_declaration?(decl)
114
+ decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
115
+ end
116
+
117
+ def root_of(decl)
118
+ decl.name.to_s.delete_prefix("::").split("::").first
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+ require_relative "../rooted_glob"
5
+ require_relative "gated_signature_plan"
6
+
7
+ module Rigor
8
+ class Environment
9
+ # Issue #2470 — a `data/vendored_gem_sigs/<gem>/` member another signature source also declares stands down,
10
+ # so the other source's declaration binds instead of the two failing the class's definition build.
11
+ #
12
+ # The vendored directories load on `RBS::EnvironmentLoader`'s batch path, ahead of the project's
13
+ # `signature_paths:` (its own `sig/`, an `rbs collection install`, a gem's `sig/`). A file there that reopens
14
+ # a vendored class and declares one of its methods again — the activerecord 8.1 collection's `patch.rbs`
15
+ # writes `def async_exec` on `PG::Connection`, where the vendored `pg.rbs` has `alias async_exec exec` — made
16
+ # `RBS::DefinitionBuilder` raise `DuplicatedMethodDefinitionError` for the class, which Rigor keeps known, so
17
+ # every call into it read `Dynamic[top]`.
18
+ #
19
+ # {.build} is the #1713 member rule ({GatedSignaturePlan}) applied to the ungated directories, planned from
20
+ # the signature files themselves rather than from a trial build: the vendored copy is Rigor's guess at a gem's
21
+ # surface, and the project chose the other source, so the other source wins (`RbsLoader.build_env_for`'s
22
+ # stated precedence). Only a directory with a member to leave out is planned; the rest stay on the batch path.
23
+ # An overload continuation (`def m: ... | ...`) declares nothing of its own and keeps the vendored base.
24
+ #
25
+ # The plan is a pure function of the vendored files and the signature files, both of which the env-cache key
26
+ # digests, so a cold and a warm run build the same environment.
27
+ module VendoredSignaturePlan
28
+ # `dirs` — the vendored directories (Pathnames) to keep off the batch path; `sources` — their files as
29
+ # `[buffer, directives, decls]`, the taken members left out, to add right after the batch; `takers` — the
30
+ # signature files a member stood down to. A member stands down only to a file that stays in the env: the
31
+ # caller re-plans without each taker the build quarantined (`RbsLoader.build_env_attempt`), or the method
32
+ # would have no declaration at all.
33
+ Plan = Data.define(:dirs, :sources, :takers)
34
+ EMPTY = Plan.new(dirs: [].freeze, sources: [].freeze, takers: Set.new.freeze)
35
+
36
+ # A `class` / `module` header, after any `%a{...}` / `%a(...)` / `%a[...]` annotations on the same line.
37
+ ANNOTATED_HEADER = /^\s*(?:%a(?:\{[^}]*\}|\([^)]*\)|\[[^\]]*\])\s*)*(?:class|module)\s+(?:::)?/
38
+
39
+ module_function
40
+
41
+ # @param roots — the ungated vendored directories this build loads.
42
+ # @param signature_files — the absolute paths of the signature files that outrank them.
43
+ # @param excluded — signature files to plan without: takers a previous build of this env quarantined.
44
+ def build(roots, signature_files, excluded = Set.new)
45
+ files = signature_files.reject { |file| excluded.include?(file) }
46
+ return EMPTY if roots.empty? || files.empty?
47
+
48
+ parsed = roots.to_h { |root| [root, parsed_sources(root)] }
49
+ taken = taken_keys(files, header_pattern(parsed.values.flatten(1)))
50
+ return EMPTY if taken.empty?
51
+
52
+ takers = Set.new
53
+ planned = parsed.filter_map do |root, sources|
54
+ kept = plan_sources(sources, taken, takers)
55
+ [root, kept] if kept
56
+ end
57
+ return EMPTY if planned.empty?
58
+
59
+ Plan.new(dirs: planned.map(&:first), sources: planned.flat_map(&:last), takers: takers.freeze)
60
+ end
61
+
62
+ # A vendored directory's files, parsed, under the Pathname buffer names the loader's batch path gives them,
63
+ # in the set `RBS::EnvironmentLoader#add(path:)` reads: every `.rbs` under it.
64
+ def parsed_sources(root)
65
+ RootedGlob.glob(root, "**/*.rbs").sort.filter_map { |file| RbsLoader.parse_signature_file(Pathname(file)) }
66
+ end
67
+
68
+ # `sources` with the taken members left out, or nil when nothing is taken from any of them.
69
+ def plan_sources(sources, taken, takers)
70
+ kept = sources.map do |buffer, directives, decls|
71
+ [buffer, directives, plan_decls(decls, nil, taken, takers)]
72
+ end
73
+ kept unless kept.zip(sources).all? { |(_, _, after), (_, _, before)| unchanged?(after, before) }
74
+ end
75
+
76
+ # A header naming one of the vendored top-level types, which every declaration of a vendored type (nested or
77
+ # `A::B`-qualified) starts from. A pre-filter only: the matches are parsed.
78
+ def header_pattern(parsed)
79
+ heads = parsed.flat_map do |_buffer, _directives, decls|
80
+ decls.filter_map { |decl| GatedSignaturePlan.type_declaration?(decl) && decl.name.to_s.delete_prefix("::") }
81
+ end
82
+ segments = heads.map { |name| Regexp.escape(name.split("::").first) }.uniq
83
+ return nil if segments.empty?
84
+
85
+ /#{ANNOTATED_HEADER}(?:#{segments.join('|')})\b/
86
+ end
87
+
88
+ # `{type name => {[method, kind] => Set[file]}}` over the signature files that declare a vendored top-level
89
+ # type.
90
+ def taken_keys(signature_files, pattern)
91
+ taken = Hash.new { |hash, name| hash[name] = Hash.new { |keys, key| keys[key] = Set.new } }
92
+ return taken if pattern.nil?
93
+
94
+ signature_files.sort.each do |file|
95
+ text = RbsLoader.read_signature_text(file)
96
+ next if text.nil? || !text.match?(pattern)
97
+
98
+ decls = RbsLoader.parse_signature_source(file, text)
99
+ collect_keys(decls, nil, taken, file) if decls
100
+ end
101
+ taken
102
+ end
103
+
104
+ def collect_keys(decls, namespace, taken, file)
105
+ decls.each do |decl|
106
+ next unless GatedSignaturePlan.type_declaration?(decl)
107
+
108
+ name = GatedSignaturePlan.qualify(namespace, decl.name)
109
+ decl.members.each do |member|
110
+ RbsLoader.member_method_keys(member).each { |key| taken[name][key] << file }
111
+ end
112
+ collect_keys(decl.members, name, taken, file)
113
+ end
114
+ end
115
+
116
+ # `decls` with every member `taken` declares for its type left out, nested types planned the same way, and
117
+ # the files each left-out member stood down to added to `takers`. A declaration nothing is taken from is
118
+ # returned as itself, which is how {.build} tells a directory unchanged.
119
+ def plan_decls(decls, namespace, taken, takers)
120
+ decls.map do |decl|
121
+ next decl unless GatedSignaturePlan.type_declaration?(decl)
122
+
123
+ plan_type(decl, GatedSignaturePlan.qualify(namespace, decl.name), taken, takers)
124
+ end
125
+ end
126
+
127
+ def unchanged?(after, before)
128
+ after.size == before.size && after.zip(before).all? { |a, b| a.equal?(b) }
129
+ end
130
+
131
+ def plan_type(decl, name, taken, takers)
132
+ keys = taken.fetch(name, nil)
133
+ members = decl.members.filter_map do |member|
134
+ if GatedSignaturePlan.type_declaration?(member)
135
+ plan_type(member, GatedSignaturePlan.qualify(name, member.name), taken, takers)
136
+ elsif keys
137
+ plan_member(member, keys, takers)
138
+ else
139
+ member
140
+ end
141
+ end
142
+ return decl if unchanged?(members, decl.members)
143
+
144
+ GatedSignaturePlan.rebuild(decl, members: members)
145
+ end
146
+
147
+ def plan_member(member, keys, takers)
148
+ RbsLoader.member_method_keys(member).each { |key| takers.merge(keys[key]) if keys.key?(key) }
149
+ GatedSignaturePlan.untaken(member, keys)
150
+ end
151
+ end
152
+ end
153
+ end