rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -32,13 +32,10 @@ module Rigor
32
32
  # `foo=`, and deliberately not `==` / `<=` / `!=` / `===`.
33
33
  ATTRIBUTE_WRITER = /\A[a-z_][A-Za-z0-9_]*=\z/
34
34
 
35
- # Every Hash method that mutates its receiver. The widening keeps two tables because they answer two different
36
- # questions of a `HashShape` — `MutationWidening::HASH_MUTATORS` changes the pair set,
37
- # `HashLookupMutation::MUTATORS` changes what a read of the pairs answers — but either is a write to the
38
- # receiver, and that is all this asks: `h.compare_by_identity` raises `FrozenError` on a frozen hash as
39
- # `h.clear` does (unless the hash already compares by identity, when it returns before the check), and a
40
- # method whose one effect was `@h.compare_by_identity` did not read as mutating while this cited the first
41
- # table alone.
35
+ # Every Hash method that mutates its receiver: the union of two widening tables that answer different
36
+ # questions of a `HashShape` (the pair set; what a read of the pairs answers). Either is a write to the
37
+ # receiver, so either counts. `compare_by_identity` lives only in the second, and it raises `FrozenError`
38
+ # on a frozen hash as `h.clear` does, so a method whose one effect is that call must still read as mutating.
42
39
  #
43
40
  # `rehash` is in neither table because it changes no read a shape can state: a shape's keys are literals, which
44
41
  # a rebuild never merges. It still rebuilds the receiver's table in place, and it is the one name spelt here.
@@ -363,7 +363,7 @@ module Rigor
363
363
  # dispatch target is spelled under a different key, and this is the rewrite: the same ancestor walk
364
364
  # {#resolve_owner} performs, on the instance side, for `initialize`.
365
365
  #
366
- # Three boundaries, and each of them can only narrow:
366
+ # Four boundaries, and each of them can only narrow:
367
367
  #
368
368
  # - a project `def self.new` **wins**. It is the definition `Const.new` actually reaches, and a
369
369
  # constructor that overrides `new` is the case where `#initialize` is not the answer. Resolved
@@ -35,8 +35,8 @@ module Rigor
35
35
  # without proposing an unrelated label for a genuinely new spelling.
36
36
  SUGGESTION_DISTANCE_CAP = 2
37
37
 
38
- # The shared registry as shipped. Memoised: the YAML parse is a once-per-process cost, and
39
- # nothing consumes the registry yet, so it stays lazy rather than paying at `require "rigor"`.
38
+ # The shared registry as shipped. Memoised: the YAML parse is a once-per-process cost, so it stays
39
+ # lazy rather than paying at `require "rigor"`.
40
40
  def self.default
41
41
  @default ||= load_file(DATA_PATH)
42
42
  end
@@ -55,7 +55,7 @@ module Rigor
55
55
  #
56
56
  # A count rather than the causes themselves (#434). On redmine the cause renderings were 144,771 of
57
57
  # the file's 293,276 bytes, on lines up to 821 characters — simultaneously the half a reviewer
58
- # cannot read and the half that churns when an unrelated call moves. This file already makes that
58
+ # cannot read and the half that churns when an unrelated call moves. {SnapshotDiff} already makes that
59
59
  # argument about origins ("a record keyed by them would churn on every refactor"); the causes are
60
60
  # the same class of thing and were the one place it was not applied. What a reviewer needs from the
61
61
  # record is the stable fact that the row is not exhaustive and by how much, which is what a count
@@ -8,8 +8,6 @@ module Rigor
8
8
  # A tainted summary reads "these effects, and possibly more": the cause says which knowledge the
9
9
  # analyzer could not have. Taint never produces a finding — diagnostics read the proven lane
10
10
  # only — so this enum is a reporting vocabulary, not a diagnostic one.
11
- #
12
- # Nothing produces causes yet; the collector of #379 is the first writer.
13
11
  module TaintCause
14
12
  # Closed and ordered by how it arises: dispatch the analyzer could not resolve first, then
15
13
  # attribution it had to take on trust, then the collector's own limits.
@@ -586,9 +586,9 @@ module Rigor
586
586
  # #1071 widens the answer per `respond_to` arm: a unit that spelled a dispatcher renders one
587
587
  # conventional `<action>.<fmt>` template per arm rather than one `<action>.html`, so the row is
588
588
  # applied once per arm the walk read, each answered outright (a `responds:` row at the arm's own
589
- # top level) or degenerate (`any` / `all`) standing its arm's edge down. The `<action>.html`
590
- # answer survives only for a dispatcher the walk could not name arms on, or one nested under a
591
- # branch — the plain fall-through still exists on the path that skips it.
589
+ # top level) or degenerate (`any` / `all`) standing its arm's edge down. The `<action>.html` answer
590
+ # is kept where no arm was read, and beside the arms when the dispatcher is nested under a branch,
591
+ # because the plain fall-through still exists on the path that skips it.
592
592
  def apply_unit_callees
593
593
  return if @responded || @singleton || @non_public
594
594
  return if @owner_class.nil? || @method_name.nil?
@@ -811,11 +811,9 @@ module Rigor
811
811
  #
812
812
  # **Unless the syntax named the class** (#463). The `Dynamic` exclusion exists because a projected
813
813
  # receiver class is a guess; a written constant path is not one, and {#catalog_target} already
814
- # trusts it for the row lookup. Refusing the posture there made the catalogue's reach a function of
815
- # whether the bundled rbs happens to ship a signature for the constant: `Net::HTTP.get` proves
816
- # `io.net.http` and `Net::IMAP.new` proved nothing, because rbs ships `net-http` and not
817
- # `net-imap`. That is why the `Net::FTP` row has never once fired, and why Redmine's IMAP and POP3
818
- # pollers proved no network at all.
814
+ # trusts it for the row lookup. Refusing the posture there made the catalogue's reach depend on
815
+ # whether the bundled rbs ships a signature for the constant: `Net::HTTP.get` proved `io.net.http`,
816
+ # while `Net::IMAP.new` proved nothing, because rbs ships `net-http` and not `net-imap`.
819
817
  #
820
818
  # The site is then **claimed**, and so discharges, like every other catalogue answer. That is not a
821
819
  # softening of the `dynamic-receiver` rule but its premise failing: the taint says the class the
@@ -974,12 +972,10 @@ module Rigor
974
972
  positional_arity(node).positive?
975
973
  end
976
974
 
977
- # The class the catalogue would look this call up under, as `[owner, singleton, implicit_self]`.
975
+ # The class the catalogue would look this call up under, as `[owner, singleton, implicit, named]`.
978
976
  # Spelled from the syntax where the syntax settles it and from the typer's receiver otherwise;
979
- # `[nil, …]` when neither does.
980
- # `[owner, singleton, implicit, named]`. `named` says the owner came from a **constant path the
981
- # author wrote**, which is what licenses the posture on a receiver the typer could not resolve
982
- # ({#posture_allowed?}).
977
+ # `[nil, …]` when neither does. `named` says the owner came from a **constant path the author wrote**,
978
+ # which is what licenses the posture on a receiver the typer could not resolve ({#posture_allowed?}).
983
979
  def catalog_target(node, record)
984
980
  receiver = node.receiver
985
981
  return ["Kernel", false, true, false] if receiver.nil? || receiver.is_a?(Prism::SelfNode)
@@ -31,7 +31,7 @@ module Rigor
31
31
  # The list is hard-coded for the MVP because it tracks rigor's bundled coverage 1:1. When a new gem is
32
32
  # vendored under `data/vendored_gem_sigs/` or added to `DEFAULT_LIBRARIES`, add its name here.
33
33
  SKIPPED_GEMS_BY_DEFAULT = Set[
34
- # DEFAULT_LIBRARIES (lib/rigor/environment.rb)
34
+ # DEFAULT_LIBRARIES (lib/rigor/environment/default_libraries.rb)
35
35
  "pathname", "optparse", "json", "yaml", "fileutils",
36
36
  "tempfile", "tmpdir", "stringio", "forwardable",
37
37
  "digest", "securerandom", "uri", "logger", "date",
@@ -100,14 +100,10 @@ module Rigor
100
100
  expected_dirs = expected_gem_dirs(locked_gems)
101
101
  filtered.select do |sig_dir|
102
102
  if git_sourced_layout?(sig_dir)
103
- # A git install's directory name carries no resolvable version (see above), so the lockfile
104
- # filter can only check the gem NAME, not `(name, version)` — but name alone is not enough: a
105
- # gem that used to be `git:`-sourced and has since moved to a released version keeps its old
106
- # `bundler/gems/<repo>-<sha>/` directory sitting in the bundle tree until a `bundle clean`, and
107
- # its NAME is still present in the lockfile (now via the rubygems entry). Matching on name only
108
- # would silently readmit that stale git install — this repo's own `vendor/bundle` had exactly
109
- # this after `binpacker` moved from `git:` to a released gem. `locked_gems` must say this name is
110
- # CURRENTLY git-sourced, not merely present.
103
+ # A git install's directory carries no version, so the lockfile filter can only check the gem NAME.
104
+ # Name alone is not enough: a gem that moved from `git:` to a released version keeps its stale
105
+ # `bundler/gems/` checkout until `bundle clean`, under a name the lockfile still lists. `locked_gems`
106
+ # must say this name is CURRENTLY git-sourced, not merely present.
111
107
  locked = locked_gems[gem_name_from_sig_path(sig_dir)]
112
108
  !!locked&.git_source
113
109
  else
@@ -9,8 +9,8 @@ module Rigor
9
9
  # This is the static fast path for built-ins; `Environment` falls through to `RbsLoader` for all other
10
10
  # names — the two tiers are complementary.
11
11
  #
12
- # See docs/internal-spec/inference-engine.md for the binding contract (every entry below MUST always be
13
- # recognised).
12
+ # See docs/internal-spec/inference-engine.md for the binding contract (the classes in
13
+ # `SLICE_1_BUILT_INS` MUST always be recognised).
14
14
  class ClassRegistry
15
15
  SLICE_1_BUILT_INS = [
16
16
  Integer,
@@ -27,7 +27,7 @@ module Rigor
27
27
  # Common Ruby core classes that user code routinely names by constant reference. Adding them to the
28
28
  # registry lets `nominal_for_name` resolve `Array`, `Hash`, etc. without each call site re-listing
29
29
  # them. The static registry is the cheap fast path; `RbsLoader` extends coverage to all other
30
- # RBS-declared names (Slice 4, landed).
30
+ # RBS-declared names.
31
31
  SLICE_2_BUILT_INS = [
32
32
  Array,
33
33
  Hash,
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../configuration_error"
4
+ require_relative "../rooted_glob"
5
+
6
+ module Rigor
7
+ class Environment
8
+ # Issue #2517 — a core signature directory chosen IN PLACE OF the `rbs` gem's own `core/` (`core_root:` in
9
+ # `.rigor.yml`). The caller is a provider such as rbs-shifter, which rewrites upstream core for an older
10
+ # CRuby (`Array#rfind` gone, `String#strip` without arguments); Rigor accepts the directory and passes it to
11
+ # `RBS::EnvironmentLoader.new(core_root:)`, so the default core is never loaded beside it.
12
+ #
13
+ # An explicit choice is never second-guessed by a fallback: a directory that is missing, unreadable or holds
14
+ # no signature, and an environment that fails to build from it, raise {CoreRootError} instead of analysing
15
+ # against the default core or against a degraded `Dynamic[top]` environment. Both would report a project as
16
+ # clean for the very APIs the selection exists to reject.
17
+ module CoreRoot
18
+ module_function
19
+
20
+ # @param path — the configured directory, already resolved against the config file's directory.
21
+ # @return the absolute directory as a frozen String (`Ractor.shareable?`, so a frozen loader stays shareable).
22
+ # @raise CoreRootError — the directory cannot serve as a core.
23
+ def validate!(path)
24
+ absolute = File.expand_path(path.to_s)
25
+ reason = invalid_reason(absolute)
26
+ raise CoreRootError, "core_root: #{absolute.inspect} #{reason}" if reason
27
+
28
+ absolute.dup.freeze
29
+ end
30
+
31
+ # A selected core inside a signature root, or one containing it, loads its files twice — once as core and once
32
+ # as project signatures — and every class it declares then fails to build on its duplicated methods.
33
+ #
34
+ # @param core_root — a {.validate!}d directory.
35
+ # @param signature_paths — the project's own signature roots.
36
+ # @raise CoreRootError — the two overlap.
37
+ def reject_overlap!(core_root, signature_paths)
38
+ core = File.join(real_path(core_root), "")
39
+ Array(signature_paths).each do |path|
40
+ root = File.join(real_path(path), "")
41
+ next unless core.start_with?(root) || root.start_with?(core)
42
+
43
+ raise CoreRootError, "core_root: #{core_root.inspect} overlaps the signature path #{path.to_s.inspect}, so " \
44
+ "its signatures would load twice; move the core outside the project's signature paths"
45
+ end
46
+ end
47
+
48
+ # Compared resolved, because the project-signature walk does not follow a symlinked directory: a `sig/core`
49
+ # link to a core kept elsewhere loads once, as core, and must not be rejected. A path that does not exist yet
50
+ # resolves through its nearest existing ancestor, so the two sides of a comparison are spelled alike.
51
+ def real_path(path)
52
+ absolute = File.expand_path(path.to_s)
53
+ missing = []
54
+ until File.exist?(absolute) || File.dirname(absolute) == absolute
55
+ missing.unshift(File.basename(absolute))
56
+ absolute = File.dirname(absolute)
57
+ end
58
+ File.join(File.realpath(absolute), *missing)
59
+ rescue SystemCallError
60
+ File.expand_path(path.to_s)
61
+ end
62
+
63
+ def invalid_reason(absolute)
64
+ return "does not exist" unless File.exist?(absolute)
65
+ return "is not a directory" unless File.directory?(absolute)
66
+ return "is not readable" unless File.readable?(absolute)
67
+
68
+ "contains no .rbs signature file" if RootedGlob.glob(absolute, "**/*.rbs").empty?
69
+ end
70
+ end
71
+
72
+ # The selected core cannot be used. A {ConfigurationError}, so the CLI reports it as one `rigor:` line with
73
+ # the usage exit code rather than as a crash.
74
+ class CoreRootError < ConfigurationError
75
+ end
76
+ end
77
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Rigor
4
4
  class Environment
5
- # Slice A stdlib expansion. Stdlib libraries that `Environment.for_project` loads on top of RBS core
5
+ # Stdlib libraries that `Environment.for_project` loads on top of RBS core
6
6
  # unless the caller passes an explicit `libraries:` array. Each entry MUST be a stdlib library name
7
7
  # accepted by `RBS::EnvironmentLoader#has_library?`; unknown libraries MUST fail-soft
8
8
  # (`RbsLoader#build_env` already filters through `has_library?`). The default set covers the common
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ module Rigor
6
+ class Environment
7
+ # Issue #2422 — what a bundled plugin's deferred signature file loads as, against the environment every other
8
+ # signature source has already built. The #610 arity check stands a file down whole first
9
+ # (`RbsLoader.generic_arity_conflict`); {.plan} decides the rest declaration by declaration and member by
10
+ # member, with {GatedSignaturePlan}'s #1713 helpers:
11
+ #
12
+ # - a method another source declares (a `def`, an attribute or an alias), where that source is NOT the project's own
13
+ # (an `rbs collection install`, a gem's `sig/`, a vendored or another plugin's file), is *composed*: the plugin's
14
+ # `def` becomes an overload continuation, `def m: <plugin overloads> | ...`, so RBS puts the plugin's overloads
15
+ # first and keeps the other source's as the fallback. The plugin's signatures are often written wider on purpose
16
+ # (`Object#html_safe?: () -> bool` where the collection says `-> false`), and standing them down would let the
17
+ # narrower one fold correct code into dead branches;
18
+ # - any other method, attribute or alias another source declares stands down: the project's own `sig/` or
19
+ # inline RBS binds by #610's rule, and an attribute, an alias or one side of a `def self?.m` cannot be
20
+ # continued ({GatedSignaturePlan.untaken} narrows an `attr_accessor` or `self?` to its untaken half);
21
+ # - a constant, global, type alias, interface or class alias another source already declares, and a type
22
+ # whose kind (class or module) contradicts another source's, stands down on its own, where adding it would
23
+ # have raised `RBS::DuplicatedDeclarationError` and dropped the plugin's whole file;
24
+ # - a class header's superclass clause another source already gives the class one stands down (#2439), where two
25
+ # different clauses would have raised `RBS::SuperclassMismatchError`; the plugin's declaration reopens the class.
26
+ #
27
+ # A declaration with nothing taken comes back as the same object, so a file with no collision loads as it
28
+ # stands. {.standdown_rows} reads what stood down back off the built environment, so a warm run off the env
29
+ # cache reports what the cold run decided.
30
+ module DeferredSignaturePlan
31
+ module_function
32
+
33
+ # @param own — the buffer names that are the project's own sources: its `signature_paths:` files (not the
34
+ # collection or gem directories also on that list) and its inline RBS.
35
+ def plan(decls, env, own, namespace = nil)
36
+ planned = decls.filter_map { |decl| plan_declaration(decl, env, own, namespace) }
37
+ same?(planned, decls) ? decls : planned
38
+ end
39
+
40
+ def plan_declaration(decl, env, own, namespace)
41
+ name = GatedSignaturePlan.qualify(namespace, declared_name(decl))
42
+ if GatedSignaturePlan.type_declaration?(decl)
43
+ return nil if kind_clash?(env, decl, name)
44
+
45
+ return plan_type(decl, env, own, name)
46
+ end
47
+ declared_elsewhere(env, decl, name) ? nil : decl
48
+ end
49
+
50
+ def plan_type(decl, env, own, name)
51
+ others = GatedSignaturePlan.other_declarations(env, name)
52
+ taken = GatedSignaturePlan.declared_keys(others)
53
+ external = others.reject { |other| own.include?(buffer_of(other)) }
54
+ defined = GatedSignaturePlan.declared_keys(external) | definition_keys(external)
55
+ members = decl.members.filter_map do |member|
56
+ next plan_declaration(member, env, own, name) if declaration?(member)
57
+ next member if taken.empty?
58
+
59
+ composed(member, defined) || GatedSignaturePlan.untaken(member, taken)
60
+ end
61
+ drop = superclass_given?(decl, others)
62
+ return decl if same?(members, decl.members) && !drop
63
+
64
+ GatedSignaturePlan.rebuild(decl, members: members, drop_super_class: drop)
65
+ end
66
+
67
+ # Issue #2439 — true when `decl` is a class with a superclass clause and another source (`others`) already
68
+ # gives the class one. Two different clauses raise `RBS::SuperclassMismatchError` and the class and every
69
+ # descendant read `Dynamic[top]`, so the plugin's header stands down and reopens the class without one. The
70
+ # two names are not compared: they are not resolved yet, and the other source's superclass binds either way.
71
+ def superclass_given?(decl, others)
72
+ super_class?(decl) && others.any? { |other| super_class?(other) }
73
+ end
74
+
75
+ # `member` as an overload continuation, or nil when it cannot be one: only a plain one-sided `def` can, and
76
+ # only onto a method — a `def`, an attribute or an alias, all of which RBS continues — that a source other
77
+ # than the project's own declares.
78
+ def composed(member, defined)
79
+ return nil unless member.is_a?(::RBS::AST::Members::MethodDefinition) && !member.overloading?
80
+ return nil if member.kind == :singleton_instance
81
+
82
+ key = [member.name, member.kind]
83
+ return nil unless defined.include?(key)
84
+
85
+ member.update(overloading: true)
86
+ end
87
+
88
+ # The `[name, kind]` keys a plain or continued `def` in `decls` declares — what a continuation can extend.
89
+ def definition_keys(decls)
90
+ decls.each_with_object(Set.new) do |decl, keys|
91
+ decl.members.each do |member|
92
+ next unless member.is_a?(::RBS::AST::Members::MethodDefinition)
93
+
94
+ sides = member.kind == :singleton_instance ? %i[instance singleton] : [member.kind]
95
+ sides.each { |side| keys << [member.name, side] }
96
+ end
97
+ end
98
+ end
99
+
100
+ def kind_clash?(env, decl, name)
101
+ type_name = ::RBS::TypeName.parse("::#{name}")
102
+ return true if env.class_alias_decls.key?(type_name)
103
+
104
+ first = GatedSignaturePlan.other_declarations(env, name).first
105
+ !first.nil? && !first.instance_of?(decl.class)
106
+ end
107
+
108
+ # The buffer of the declaration another source already makes under `decl`'s name, or nil.
109
+ def declared_elsewhere(env, decl, name)
110
+ entry = declaration_entry(env, decl, name)
111
+ entry && (RbsLoader.declaration_buffer_name(entry.decl) || "")
112
+ end
113
+
114
+ def declaration_entry(env, decl, name)
115
+ type_name = -> { ::RBS::TypeName.parse("::#{name}") }
116
+ case decl
117
+ when ::RBS::AST::Declarations::Constant then env.constant_decls[type_name.call]
118
+ when ::RBS::AST::Declarations::Global then env.global_decls[decl.name]
119
+ when ::RBS::AST::Declarations::TypeAlias then env.type_alias_decls[type_name.call]
120
+ when ::RBS::AST::Declarations::Interface then env.interface_decls[type_name.call]
121
+ when ::RBS::AST::Declarations::ClassAlias, ::RBS::AST::Declarations::ModuleAlias
122
+ env.class_alias_decls[type_name.call] || env.class_decls[type_name.call]
123
+ end
124
+ end
125
+
126
+ # A class or module alias names itself as `new_name`; every other declaration as `name`.
127
+ def declared_name(decl)
128
+ decl.respond_to?(:new_name) ? decl.new_name : decl.name
129
+ end
130
+
131
+ def declaration?(member)
132
+ member.is_a?(::RBS::AST::Declarations::Base)
133
+ end
134
+
135
+ def same?(planned, original)
136
+ planned.size == original.size && planned.zip(original).all? { |a, b| a.equal?(b) }
137
+ end
138
+
139
+ def buffer_of(decl)
140
+ RbsLoader.declaration_buffer_name(decl)
141
+ end
142
+
143
+ # One {.standdown_rows} walk over a deferred file: the built env, the file's buffer name, the rows so far, and
144
+ # the env's type-name resolver, built on first use (only a dropped superclass clause needs it).
145
+ class Walk
146
+ attr_reader :env, :file, :rows
147
+
148
+ def initialize(env, file)
149
+ @env = env
150
+ @file = file
151
+ @rows = []
152
+ end
153
+
154
+ # rbs 4.x builds the resolver with `TypeNameResolver.build(env)`; rbs 3.x's constructor takes the env, which
155
+ # the 4.x signature Rigor reads (`new(all_names, aliases)`) does not describe.
156
+ def resolver
157
+ @resolver ||= begin
158
+ klass = ::RBS::Resolver::TypeNameResolver
159
+ if klass.respond_to?(:build)
160
+ klass.build(env)
161
+ else
162
+ klass.new(env) # rigor:disable call.wrong-arity
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ # What {.plan} left out of the deferred `file` (its parsed `decls`) in the built `env`, as `[declaration,
169
+ # cause_file]` rows — `Object#acts_like?`, `Time.at`, `FFI::Pointer::NULL`, `Foo < Bar`, or a type name — with
170
+ # the buffer of the other source's declaration, or nil when it cannot be named. A composed method is not a row:
171
+ # the plugin's overloads still bind.
172
+ def standdown_rows(env, file, decls)
173
+ walk = Walk.new(env, file)
174
+ decls.each { |decl| declaration_rows(walk, decl, nil) }
175
+ walk.rows.uniq
176
+ end
177
+
178
+ # `context` is RBS's resolver context for where `decl` is written: nil at the top level, else `[outer,
179
+ # absolute name of the enclosing declaration]`, as `RBS::Environment#append_context` builds it.
180
+ def declaration_rows(walk, decl, context)
181
+ name = GatedSignaturePlan.qualify(namespace_of(context), declared_name(decl))
182
+ return type_rows(walk, decl, name, context) if GatedSignaturePlan.type_declaration?(decl)
183
+
184
+ cause = displaced_by(walk.env, decl, name, walk.file)
185
+ walk.rows << [decl.is_a?(::RBS::AST::Declarations::Global) ? decl.name.to_s : name, cause] if cause
186
+ end
187
+
188
+ def namespace_of(context)
189
+ context && context.last.to_s.delete_prefix("::")
190
+ end
191
+
192
+ # A non-nil buffer name (possibly empty) when another source's declaration of `decl` is the one loaded.
193
+ def displaced_by(env, decl, name, file)
194
+ entry = declaration_entry(env, decl, name)
195
+ return nil if entry.nil?
196
+
197
+ buffer = RbsLoader.declaration_buffer_name(entry.decl)
198
+ buffer == file ? nil : buffer
199
+ end
200
+
201
+ def type_rows(walk, decl, name, context)
202
+ loaded, others = loaded_and_others(walk, name)
203
+ if loaded.empty?
204
+ cause = others.empty? ? alias_cause(walk.env, name) : buffer_of(others.first)
205
+ walk.rows << [name, cause] if cause
206
+ return
207
+ end
208
+
209
+ walk.rows.concat(superclass_rows(walk, decl, name, context, others))
210
+ walk.rows.concat(member_rows(decl, name, loaded, others))
211
+ inner = [context, ::RBS::TypeName.parse("::#{name}")]
212
+ decl.members.each { |member| declaration_rows(walk, member, inner) if declaration?(member) }
213
+ end
214
+
215
+ # The declarations of `name` the walked file loaded, and the ones other sources made (synthesized stubs left out).
216
+ def loaded_and_others(walk, name)
217
+ entry = walk.env.class_decls[::RBS::TypeName.parse("::#{name}")]
218
+ declarations = entry.nil? ? [] : RbsLoader.entry_declarations(entry)
219
+ loaded = declarations.select { |other| buffer_of(other) == walk.file }
220
+ [loaded, declarations.reject { |other| loaded.include?(other) || GatedSignaturePlan.synthetic?(other) }]
221
+ end
222
+
223
+ # The buffer of a class or module alias declaring `name` (empty when it cannot be named), or nil.
224
+ def alias_cause(env, name)
225
+ entry = env.class_alias_decls[::RBS::TypeName.parse("::#{name}")]
226
+ entry && (RbsLoader.declaration_buffer_name(entry.decl) || "")
227
+ end
228
+
229
+ # The plugin's superclass clause as a row (`ActiveRecord::StatementInvalid < ActiveRecordError`) when the plan
230
+ # dropped it for another source's ({.superclass_given?}) and that source's names a different class or type
231
+ # arguments. The plan drops an identical clause too, but nothing is lost, so no row.
232
+ def superclass_rows(walk, decl, name, context, others)
233
+ return [] unless superclass_given?(decl, others)
234
+
235
+ cause = others.find { |other| super_class?(other) }
236
+ return [] if same_super_class?(walk, decl.super_class, context, cause.super_class)
237
+
238
+ written = decl.super_class
239
+ args = written.args.empty? ? "" : "[#{written.args.join(', ')}]"
240
+ [["#{name} < #{written.name}#{args}", buffer_of(cause)]]
241
+ end
242
+
243
+ # Whether the plugin's clause `written`, resolved as RBS resolves a superclass (in the context the class is
244
+ # written in, not its own), names the class and type arguments of `other`, the built env's already-absolute
245
+ # clause, class aliases normalised on both sides. An unresolvable name differs.
246
+ def same_super_class?(walk, written, context, other)
247
+ name = absolute_name(walk, written.name, context)
248
+ return false if name.nil? || normalized(walk.env, name) != normalized(walk.env, other.name)
249
+
250
+ written.args.map { |arg| normalized_type(walk, arg, context) } ==
251
+ other.args.map { |arg| normalized_type(walk, arg, nil) }
252
+ end
253
+
254
+ def absolute_name(walk, name, context)
255
+ name.absolute? ? name : walk.resolver.resolve(name, context: context)
256
+ end
257
+
258
+ def normalized(env, name)
259
+ name.class? ? env.normalize_module_name(name) : name
260
+ end
261
+
262
+ def normalized_type(walk, type, context)
263
+ return type.to_s unless type.respond_to?(:map_type_name)
264
+
265
+ type.map_type_name { |name, _, _| normalized(walk.env, absolute_name(walk, name, context) || name) }.to_s
266
+ end
267
+
268
+ def super_class?(decl)
269
+ decl.is_a?(::RBS::AST::Declarations::Class) && !decl.super_class.nil?
270
+ end
271
+
272
+ def member_rows(decl, name, loaded, others)
273
+ present = GatedSignaturePlan.declared_keys(loaded) | definition_keys(loaded)
274
+ decl.members.flat_map do |member|
275
+ RbsLoader.member_method_keys(member).reject { |key| present.include?(key) }.filter_map do |key|
276
+ cause = others.find { |other| other.members.any? { |m| RbsLoader.member_method_keys(m).include?(key) } }
277
+ cause && [GatedSignaturePlan.member_label(name, key), buffer_of(cause)]
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -13,7 +13,7 @@ module Rigor
13
13
  module GatedSignatureGuard
14
14
  module_function
15
15
 
16
- # @param dir — the vendored directory (Pathname).
16
+ # @param dir — the vendored directory.
17
17
  # @return the absolute-less names (`"Prime::PseudoPrimeGenerator"`) of every class and module it declares.
18
18
  def type_names(dir)
19
19
  RootedGlob.glob(dir, "*.rbs").flat_map do |file|
@@ -213,10 +213,13 @@ module Rigor
213
213
  end
214
214
  end
215
215
 
216
- def rebuild(decl, members:)
216
+ # `decl` with `members` in place of its own. A class keeps its superclass unless `drop_super_class` (#2439: a
217
+ # plugin's class header another source already gives a superclass reopens the class without one).
218
+ def rebuild(decl, members:, drop_super_class: false)
217
219
  if decl.is_a?(::RBS::AST::Declarations::Class)
218
220
  ::RBS::AST::Declarations::Class.new(
219
- name: decl.name, type_params: decl.type_params, super_class: decl.super_class, members: members,
221
+ name: decl.name, type_params: decl.type_params, super_class: drop_super_class ? nil : decl.super_class,
222
+ members: members,
220
223
  annotations: decl.annotations, location: decl.location, comment: decl.comment
221
224
  )
222
225
  else
@@ -7,7 +7,7 @@ module Rigor
7
7
  # Gemfile.lock parser for target-project bundler awareness (O4 Layer 3, implemented).
8
8
  #
9
9
  # Parses a target project's `Gemfile.lock` via Bundler's `LockfileParser` and exposes the locked gem set
10
- # as a frozen `Hash[String, LockfileResolver::LockedGem]` keyed by gem name. Used by
10
+ # as a frozen map from gem name to `LockedGem`. Used by
11
11
  # {Rigor::Environment::BundleSigDiscovery} as a filter so the discovered `sig/` directories under the
12
12
  # bundler install root are limited to gems the project actually declares (and at the version it declared
13
13
  # them).
@@ -38,8 +38,12 @@ module Rigor
38
38
  # variant for the readers that want a single version (coverage report, doctor): the `ruby` one when the
39
39
  # lockfile has it, otherwise the last parsed. Defaults to the single `[version, platform]` pair, and the
40
40
  # pair must be one of `variants`.
41
- LockedGem = Data.define(:name, :version, :platform, :git_source, :variants) do
42
- def initialize(name:, version:, platform:, git_source: false, variants: nil)
41
+ #
42
+ # `path_source` is the absolute directory a lockfile `PATH` stanza resolved this gem from (its `remote:`
43
+ # expanded against the lockfile's directory), and `nil` for every other source, `GIT` included. Issue
44
+ # #2441 reads it to tell the project's own path gems apart from the rest of the bundle. Defaults `nil`.
45
+ LockedGem = Data.define(:name, :version, :platform, :git_source, :variants, :path_source) do
46
+ def initialize(name:, version:, platform:, git_source: false, variants: nil, path_source: nil)
43
47
  version = -version.to_s
44
48
  platform = -platform.to_s
45
49
  variants = (variants || [[version, platform]]).map { |v, p| [-v.to_s, -p.to_s].freeze }.uniq.freeze
@@ -52,7 +56,8 @@ module Rigor
52
56
  version: version,
53
57
  platform: platform,
54
58
  git_source: git_source ? true : false,
55
- variants: variants
59
+ variants: variants,
60
+ path_source: path_source && -path_source.to_s
56
61
  )
57
62
  end
58
63
  end
@@ -115,9 +120,9 @@ module Rigor
115
120
  def self.do_parse_dependency_names(path)
116
121
  parser = Bundler::LockfileParser.new(File.read(path.to_s))
117
122
  # `#dependencies` is the parsed `DEPENDENCIES` section keyed by gem name, so the version constraint
118
- # and the pinned-source `!` suffix are already stripped for us. The upstream bundler RBS shim does
119
- # not declare it.
120
- parser.dependencies.keys.to_set { |name| -name.to_s }.freeze # rigor:disable undefined-method
123
+ # and the pinned-source `!` suffix are already stripped for us. Rigor's vendored bundler
124
+ # signature declares it as a Hash keyed by gem name.
125
+ parser.dependencies.keys.to_set { |name| -name.to_s }.freeze
121
126
  rescue ::Ractor::IsolationError => e
122
127
  warn_isolated(path, e)
123
128
  EMPTY_NAMES
@@ -171,10 +176,9 @@ module Rigor
171
176
  parser = Bundler::LockfileParser.new(body)
172
177
  locked = parser.specs.each_with_object({}) do |spec, h|
173
178
  # `Bundler::LazySpecification` carries name, version, platform. Platform is `Gem::Platform` or the
174
- # symbol `:ruby`; both stringify cleanly. The upstream bundler RBS shim
175
- # (references/rbs/sig/shims/bundler.rbs) does NOT declare `LazySpecification#platform` so the call
176
- # site needs a suppression marker.
177
- platform = spec.platform.to_s # rigor:disable undefined-method
179
+ # symbol `:ruby`; both stringify cleanly. Rigor's vendored bundler signature declares
180
+ # `LazySpecification#platform`, so the call needs no suppression.
181
+ platform = spec.platform.to_s
178
182
  # `#source` is the `Bundler::Source::*` this spec resolved from — `Git` for a `GIT` lockfile
179
183
  # stanza, `Rubygems` for a `GEM` one, `Path`/`Gemspec` for `path:`. Only `Git` sources land under
180
184
  # {BundleSigDiscovery}'s `bundler/gems/` walk.
@@ -182,7 +186,8 @@ module Rigor
182
186
  # instead of letting the last one overwrite the rest (#1844).
183
187
  entry = LockedGem.new(
184
188
  name: spec.name, version: spec.version.to_s, platform: platform,
185
- git_source: spec.source.is_a?(Bundler::Source::Git)
189
+ git_source: spec.source.is_a?(Bundler::Source::Git),
190
+ path_source: path_source_dir(spec.source, path)
186
191
  )
187
192
  h[entry.name] = (prior = h[entry.name]) ? merge_variant(prior, entry) : entry
188
193
  end
@@ -196,6 +201,17 @@ module Rigor
196
201
  end
197
202
  private_class_method :do_parse
198
203
 
204
+ # The absolute directory a `PATH` stanza names, resolved against the lockfile's own directory — never
205
+ # against `Bundler.root`, which is Rigor's process and not the analysed project. `Bundler::Source::Git`
206
+ # subclasses `Path`, so a `GIT` stanza is excluded by name.
207
+ def self.path_source_dir(source, lockfile_path)
208
+ return nil unless source.is_a?(Bundler::Source::Path)
209
+ return nil if source.is_a?(Bundler::Source::Git)
210
+
211
+ File.expand_path(source.path.to_s, File.dirname(File.expand_path(lockfile_path.to_s)))
212
+ end
213
+ private_class_method :path_source_dir
214
+
199
215
  # Folds a further platform variant of an already-parsed name into it. The representative keeps a `ruby`
200
216
  # variant once seen; otherwise the last variant wins, as it did before variants were kept.
201
217
  def self.merge_variant(prior, entry)